diff --git a/packages/react-native-codegen/lib/CodegenSchema.d.ts b/packages/react-native-codegen/lib/CodegenSchema.d.ts new file mode 100644 index 00000000000000..fc13854cb0dda2 --- /dev/null +++ b/packages/react-native-codegen/lib/CodegenSchema.d.ts @@ -0,0 +1,397 @@ +/** + * 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. + */ + +export type PlatformType = + | 'iOS' + | 'android'; + +export interface SchemaType { + readonly modules: { + [hasteModuleName: string]: ComponentSchema | NativeModuleSchema; + }; +} + +/** + * Component Type Annotations + */ +export interface DoubleTypeAnnotation { + readonly type: 'DoubleTypeAnnotation'; +} + +export interface FloatTypeAnnotation { + readonly type: 'FloatTypeAnnotation'; +} + +export interface BooleanTypeAnnotation { + readonly type: 'BooleanTypeAnnotation'; +} + +export interface Int32TypeAnnotation { + readonly type: 'Int32TypeAnnotation'; +} + +export interface StringTypeAnnotation { + readonly type: 'StringTypeAnnotation'; +} + +export interface StringEnumTypeAnnotation { + readonly type: 'StringEnumTypeAnnotation'; + readonly options: readonly string[]; +} + +export interface VoidTypeAnnotation { + readonly type: 'VoidTypeAnnotation'; +} + +export interface ObjectTypeAnnotation { + readonly type: 'ObjectTypeAnnotation'; + readonly properties: readonly NamedShape[]; + // metadata for objects that generated from interfaces + readonly baseTypes?: readonly string[] | undefined; +} + +export interface MixedTypeAnnotation { + readonly type: 'MixedTypeAnnotation'; +} + +export interface EventEmitterTypeAnnotation { + readonly type: 'EventEmitterTypeAnnotation'; + readonly typeAnnotation: NativeModuleBaseTypeAnnotation; +} + +export interface FunctionTypeAnnotation { + readonly type: 'FunctionTypeAnnotation'; + readonly params: readonly NamedShape

[]; + readonly returnTypeAnnotation: R; +} + +export interface NamedShape { + readonly name: string; + readonly optional: boolean; + readonly typeAnnotation: T; +} + +export interface ComponentSchema { + readonly type: 'Component'; + readonly components: { + [componentName: string]: ComponentShape; + }; +} + +export interface ComponentShape extends OptionsShape { + readonly extendsProps: readonly ExtendsPropsShape[]; + readonly events: readonly EventTypeShape[]; + readonly props: readonly NamedShape[]; + readonly commands: readonly NamedShape[]; + readonly deprecatedViewConfigName?: string | undefined; +} + +export interface OptionsShape { + readonly interfaceOnly?: boolean | undefined; + // Use for components with no current paper rename in progress + // Does not check for new name + readonly paperComponentName?: string | undefined; + // Use for components that are not used on other platforms. + readonly excludedPlatforms?: readonly PlatformType[] | undefined; + // Use for components currently being renamed in paper + // Will use new name if it is available and fallback to this name + readonly paperComponentNameDeprecated?: string | undefined; +} + +export interface ExtendsPropsShape { + readonly type: 'ReactNativeBuiltInType'; + readonly knownTypeName: 'ReactNativeCoreViewProps'; +} + +export interface EventTypeShape { + readonly name: string; + readonly bubblingType: + | 'direct' + | 'bubble'; + readonly optional: boolean; + readonly paperTopLevelNameDeprecated?: string | undefined; + readonly typeAnnotation: { + readonly type: 'EventTypeAnnotation'; + readonly argument?: ObjectTypeAnnotation | undefined; + }; +} + +export type EventTypeAnnotation = + | BooleanTypeAnnotation + | StringTypeAnnotation + | DoubleTypeAnnotation + | FloatTypeAnnotation + | Int32TypeAnnotation + | MixedTypeAnnotation + | StringEnumTypeAnnotation + | ObjectTypeAnnotation + | { + readonly type: 'ArrayTypeAnnotation'; + readonly elementType: EventTypeAnnotation + }; + +export type ArrayTypeAnnotation = { + readonly type: 'ArrayTypeAnnotation'; + readonly elementType: + | BooleanTypeAnnotation + | StringTypeAnnotation + | DoubleTypeAnnotation + | FloatTypeAnnotation + | Int32TypeAnnotation + | { + readonly type: 'StringEnumTypeAnnotation'; + readonly default: string; + readonly options: readonly string[]; + } + | ObjectTypeAnnotation + | ReservedPropTypeAnnotation + | { + readonly type: 'ArrayTypeAnnotation'; + readonly elementType: ObjectTypeAnnotation; + }; +} + +export type PropTypeAnnotation = + | { + readonly type: 'BooleanTypeAnnotation'; + readonly default: boolean | null; + } + | { + readonly type: 'StringTypeAnnotation'; + readonly default: string | null; + } + | { + readonly type: 'DoubleTypeAnnotation'; + readonly default: number; + } + | { + readonly type: 'FloatTypeAnnotation'; + readonly default: number | null; + } + | { + readonly type: 'Int32TypeAnnotation'; + readonly default: number; + } + | { + readonly type: 'StringEnumTypeAnnotation'; + readonly default: string; + readonly options: readonly string[]; + } + | { + readonly type: 'Int32EnumTypeAnnotation'; + readonly default: number; + readonly options: readonly number[]; + } + | ReservedPropTypeAnnotation + | ObjectTypeAnnotation + | ArrayTypeAnnotation + | MixedTypeAnnotation; + +export interface ReservedPropTypeAnnotation { + readonly type: 'ReservedPropTypeAnnotation'; + readonly name: + | 'ColorPrimitive' + | 'ImageSourcePrimitive' + | 'PointPrimitive' + | 'EdgeInsetsPrimitive' + | 'ImageRequestPrimitive' + | 'DimensionPrimitive'; +} + +export type CommandTypeAnnotation = FunctionTypeAnnotation< + CommandParamTypeAnnotation, + VoidTypeAnnotation +>; + +export type CommandParamTypeAnnotation = + | ReservedTypeAnnotation + | BooleanTypeAnnotation + | Int32TypeAnnotation + | DoubleTypeAnnotation + | FloatTypeAnnotation + | StringTypeAnnotation + | ArrayTypeAnnotation; + +export interface ReservedTypeAnnotation { + readonly type: 'ReservedTypeAnnotation'; + readonly name: 'RootTag'; // Union with more custom types. +} + +/** + * NativeModule Types + */ +export type Nullable = + | NullableTypeAnnotation + | T; + +export interface NullableTypeAnnotation { + readonly type: 'NullableTypeAnnotation'; + readonly typeAnnotation: T; +} + +export interface NativeModuleSchema { + readonly type: 'NativeModule'; + readonly aliasMap: NativeModuleAliasMap; + readonly enumMap: NativeModuleEnumMap; + readonly spec: NativeModuleSpec; + readonly moduleName: string; + // Use for modules that are not used on other platforms. + // TODO: It's clearer to define `restrictedToPlatforms` instead, but + // `excludedPlatforms` is used here to be consistent with ComponentSchema. + readonly excludedPlatforms?: readonly PlatformType[] | undefined; +} + +export interface NativeModuleSpec { + readonly eventEmitters: readonly NativeModuleEventEmitterShape[]; + readonly methods: readonly NativeModulePropertyShape[]; +} + +export type NativeModulePropertyShape = NamedShape< + Nullable +>; + +export type NativeModuleEventEmitterShape = NamedShape; + +export interface NativeModuleEnumMap { + readonly [enumName: string]: NativeModuleEnumDeclarationWithMembers; +} + +export interface NativeModuleAliasMap { + readonly [aliasName: string]: NativeModuleObjectTypeAnnotation; +} + +export type NativeModuleFunctionTypeAnnotation = FunctionTypeAnnotation< + Nullable, + Nullable +>; + +export type NativeModuleObjectTypeAnnotation = ObjectTypeAnnotation< + Nullable +>; + +export interface NativeModuleArrayTypeAnnotation> { + readonly type: 'ArrayTypeAnnotation'; + /** + * TODO(T72031674): Migrate all our NativeModule specs to not use + * invalid Array ElementTypes. Then, make the elementType required. + */ + readonly elementType?: T | undefined; +} + +export interface NativeModuleStringTypeAnnotation { + readonly type: 'StringTypeAnnotation'; +} + +export interface NativeModuleNumberTypeAnnotation { + readonly type: 'NumberTypeAnnotation'; +} + +export interface NativeModuleInt32TypeAnnotation { + readonly type: 'Int32TypeAnnotation'; +} + +export interface NativeModuleDoubleTypeAnnotation { + readonly type: 'DoubleTypeAnnotation'; +} + +export interface NativeModuleFloatTypeAnnotation { + readonly type: 'FloatTypeAnnotation'; +} + +export interface NativeModuleBooleanTypeAnnotation { + readonly type: 'BooleanTypeAnnotation'; +} + +export type NativeModuleEnumMembers = readonly { + readonly name: string; + readonly value: string | number; +}[]; + +export type NativeModuleEnumMemberType = + | 'NumberTypeAnnotation' + | 'StringTypeAnnotation'; + +export interface NativeModuleEnumDeclaration { + readonly name: string; + readonly type: 'EnumDeclaration'; + readonly memberType: NativeModuleEnumMemberType; +} + +export interface NativeModuleEnumDeclarationWithMembers { + name: string; + type: 'EnumDeclarationWithMembers'; + memberType: NativeModuleEnumMemberType; + members: NativeModuleEnumMembers; +} + +export interface NativeModuleGenericObjectTypeAnnotation { + readonly type: 'GenericObjectTypeAnnotation'; + // a dictionary type is codegen as "Object" + // but we know all its members are in the same type + // when it happens, the following field is non-null + readonly dictionaryValueType?: Nullable | undefined; +} + +export interface NativeModuleTypeAliasTypeAnnotation { + readonly type: 'TypeAliasTypeAnnotation'; + readonly name: string; +} + +export interface NativeModulePromiseTypeAnnotation { + readonly type: 'PromiseTypeAnnotation'; + readonly elementType?: Nullable | undefined; +} + +export type UnionTypeAnnotationMemberType = + | 'NumberTypeAnnotation' + | 'ObjectTypeAnnotation' + | 'StringTypeAnnotation'; + +export interface NativeModuleUnionTypeAnnotation { + readonly type: 'UnionTypeAnnotation'; + readonly memberType: UnionTypeAnnotationMemberType; +} + +export interface NativeModuleMixedTypeAnnotation { + readonly type: 'MixedTypeAnnotation'; +} + +export type NativeModuleBaseTypeAnnotation = + | NativeModuleStringTypeAnnotation + | NativeModuleNumberTypeAnnotation + | NativeModuleInt32TypeAnnotation + | NativeModuleDoubleTypeAnnotation + | NativeModuleFloatTypeAnnotation + | NativeModuleBooleanTypeAnnotation + | NativeModuleEnumDeclaration + | NativeModuleGenericObjectTypeAnnotation + | ReservedTypeAnnotation + | NativeModuleTypeAliasTypeAnnotation + | NativeModuleArrayTypeAnnotation> + | NativeModuleObjectTypeAnnotation + | NativeModuleUnionTypeAnnotation + | NativeModuleMixedTypeAnnotation; + +export type NativeModuleParamTypeAnnotation = + | NativeModuleBaseTypeAnnotation + | NativeModuleParamOnlyTypeAnnotation; + +export type NativeModuleReturnTypeAnnotation = + | NativeModuleBaseTypeAnnotation + | NativeModuleReturnOnlyTypeAnnotation; + +export type NativeModuleTypeAnnotation = + | NativeModuleBaseTypeAnnotation + | NativeModuleParamOnlyTypeAnnotation + | NativeModuleReturnOnlyTypeAnnotation; + +type NativeModuleParamOnlyTypeAnnotation = NativeModuleFunctionTypeAnnotation; + +type NativeModuleReturnOnlyTypeAnnotation = + | NativeModuleFunctionTypeAnnotation + | NativeModulePromiseTypeAnnotation + | VoidTypeAnnotation; diff --git a/packages/react-native-codegen/lib/CodegenSchema.js b/packages/react-native-codegen/lib/CodegenSchema.js new file mode 100644 index 00000000000000..4949d716cda2e2 --- /dev/null +++ b/packages/react-native-codegen/lib/CodegenSchema.js @@ -0,0 +1,11 @@ +/** + * 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'; diff --git a/packages/react-native-codegen/lib/CodegenSchema.js.flow b/packages/react-native-codegen/lib/CodegenSchema.js.flow new file mode 100644 index 00000000000000..d53c39421597a8 --- /dev/null +++ b/packages/react-native-codegen/lib/CodegenSchema.js.flow @@ -0,0 +1,402 @@ +/** + * 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'; + +export type PlatformType = 'iOS' | 'android'; + +export type SchemaType = $ReadOnly<{ + modules: $ReadOnly<{ + [hasteModuleName: string]: ComponentSchema | NativeModuleSchema, + }>, +}>; + +/** + * Component Type Annotations + */ +export type DoubleTypeAnnotation = $ReadOnly<{ + type: 'DoubleTypeAnnotation', +}>; + +export type FloatTypeAnnotation = $ReadOnly<{ + type: 'FloatTypeAnnotation', +}>; + +export type BooleanTypeAnnotation = $ReadOnly<{ + type: 'BooleanTypeAnnotation', +}>; + +export type Int32TypeAnnotation = $ReadOnly<{ + type: 'Int32TypeAnnotation', +}>; + +export type StringTypeAnnotation = $ReadOnly<{ + type: 'StringTypeAnnotation', +}>; + +export type StringEnumTypeAnnotation = $ReadOnly<{ + type: 'StringEnumTypeAnnotation', + options: $ReadOnlyArray, +}>; + +export type VoidTypeAnnotation = $ReadOnly<{ + type: 'VoidTypeAnnotation', +}>; + +export type ObjectTypeAnnotation<+T> = $ReadOnly<{ + type: 'ObjectTypeAnnotation', + properties: $ReadOnlyArray>, + // metadata for objects that generated from interfaces + baseTypes?: $ReadOnlyArray, +}>; + +export type MixedTypeAnnotation = $ReadOnly<{ + type: 'MixedTypeAnnotation', +}>; + +type EventEmitterTypeAnnotation = $ReadOnly<{ + type: 'EventEmitterTypeAnnotation', + typeAnnotation: NativeModuleBaseTypeAnnotation, +}>; + +type FunctionTypeAnnotation<+P, +R> = $ReadOnly<{ + type: 'FunctionTypeAnnotation', + params: $ReadOnlyArray>, + returnTypeAnnotation: R, +}>; + +export type NamedShape<+T> = $ReadOnly<{ + name: string, + optional: boolean, + typeAnnotation: T, +}>; + +export type ComponentSchema = $ReadOnly<{ + type: 'Component', + components: $ReadOnly<{ + [componentName: string]: ComponentShape, + }>, +}>; + +export type ComponentShape = $ReadOnly<{ + ...OptionsShape, + extendsProps: $ReadOnlyArray, + events: $ReadOnlyArray, + props: $ReadOnlyArray>, + commands: $ReadOnlyArray>, +}>; + +export type OptionsShape = $ReadOnly<{ + interfaceOnly?: boolean, + // Use for components with no current paper rename in progress + // Does not check for new name + paperComponentName?: string, + // Use for components that are not used on other platforms. + excludedPlatforms?: $ReadOnlyArray, + // Use for components currently being renamed in paper + // Will use new name if it is available and fallback to this name + paperComponentNameDeprecated?: string, +}>; + +export type ExtendsPropsShape = $ReadOnly<{ + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', +}>; + +export type EventTypeShape = $ReadOnly<{ + name: string, + bubblingType: 'direct' | 'bubble', + optional: boolean, + paperTopLevelNameDeprecated?: string, + typeAnnotation: $ReadOnly<{ + type: 'EventTypeAnnotation', + argument?: ObjectTypeAnnotation, + }>, +}>; + +export type EventTypeAnnotation = + | BooleanTypeAnnotation + | StringTypeAnnotation + | DoubleTypeAnnotation + | FloatTypeAnnotation + | Int32TypeAnnotation + | MixedTypeAnnotation + | StringEnumTypeAnnotation + | ObjectTypeAnnotation + | $ReadOnly<{ + type: 'ArrayTypeAnnotation', + elementType: EventTypeAnnotation, + }>; + +export type ArrayTypeAnnotation = $ReadOnly<{ + type: 'ArrayTypeAnnotation', + elementType: + | BooleanTypeAnnotation + | StringTypeAnnotation + | DoubleTypeAnnotation + | FloatTypeAnnotation + | Int32TypeAnnotation + | $ReadOnly<{ + type: 'StringEnumTypeAnnotation', + default: string, + options: $ReadOnlyArray, + }> + | ObjectTypeAnnotation + | ReservedPropTypeAnnotation + | $ReadOnly<{ + type: 'ArrayTypeAnnotation', + elementType: ObjectTypeAnnotation, + }>, +}>; + +export type PropTypeAnnotation = + | $ReadOnly<{ + type: 'BooleanTypeAnnotation', + default: boolean | null, + }> + | $ReadOnly<{ + type: 'StringTypeAnnotation', + default: string | null, + }> + | $ReadOnly<{ + type: 'DoubleTypeAnnotation', + default: number, + }> + | $ReadOnly<{ + type: 'FloatTypeAnnotation', + default: number | null, + }> + | $ReadOnly<{ + type: 'Int32TypeAnnotation', + default: number, + }> + | $ReadOnly<{ + type: 'StringEnumTypeAnnotation', + default: string, + options: $ReadOnlyArray, + }> + | $ReadOnly<{ + type: 'Int32EnumTypeAnnotation', + default: number, + options: $ReadOnlyArray, + }> + | ReservedPropTypeAnnotation + | ObjectTypeAnnotation + | ArrayTypeAnnotation + | MixedTypeAnnotation; + +export type ReservedPropTypeAnnotation = $ReadOnly<{ + type: 'ReservedPropTypeAnnotation', + name: + | 'ColorPrimitive' + | 'ImageSourcePrimitive' + | 'PointPrimitive' + | 'EdgeInsetsPrimitive' + | 'ImageRequestPrimitive' + | 'DimensionPrimitive', +}>; + +export type CommandTypeAnnotation = FunctionTypeAnnotation< + CommandParamTypeAnnotation, + VoidTypeAnnotation, +>; + +export type CommandParamTypeAnnotation = + | ReservedTypeAnnotation + | BooleanTypeAnnotation + | Int32TypeAnnotation + | DoubleTypeAnnotation + | FloatTypeAnnotation + | StringTypeAnnotation + | ArrayTypeAnnotation; + +export type ReservedTypeAnnotation = $ReadOnly<{ + type: 'ReservedTypeAnnotation', + name: 'RootTag', // Union with more custom types. +}>; + +/** + * NativeModule Types + */ +export type Nullable<+T: NativeModuleTypeAnnotation> = + | NullableTypeAnnotation + | T; + +export type NullableTypeAnnotation<+T: NativeModuleTypeAnnotation> = $ReadOnly<{ + type: 'NullableTypeAnnotation', + typeAnnotation: T, +}>; + +export type NativeModuleSchema = $ReadOnly<{ + type: 'NativeModule', + aliasMap: NativeModuleAliasMap, + enumMap: NativeModuleEnumMap, + spec: NativeModuleSpec, + moduleName: string, + // Use for modules that are not used on other platforms. + // TODO: It's clearer to define `restrictedToPlatforms` instead, but + // `excludedPlatforms` is used here to be consistent with ComponentSchema. + excludedPlatforms?: $ReadOnlyArray, +}>; + +type NativeModuleSpec = $ReadOnly<{ + eventEmitters: $ReadOnlyArray, + methods: $ReadOnlyArray, +}>; + +export type NativeModuleEventEmitterShape = + NamedShape; + +export type NativeModulePropertyShape = NamedShape< + Nullable, +>; + +export type NativeModuleEnumMap = $ReadOnly<{ + [enumName: string]: NativeModuleEnumDeclarationWithMembers, +}>; + +export type NativeModuleAliasMap = $ReadOnly<{ + [aliasName: string]: NativeModuleObjectTypeAnnotation, +}>; + +export type NativeModuleFunctionTypeAnnotation = FunctionTypeAnnotation< + Nullable, + Nullable, +>; + +export type NativeModuleObjectTypeAnnotation = ObjectTypeAnnotation< + Nullable, +>; + +export type NativeModuleArrayTypeAnnotation< + +T: Nullable, +> = $ReadOnly<{ + type: 'ArrayTypeAnnotation', + /** + * TODO(T72031674): Migrate all our NativeModule specs to not use + * invalid Array ElementTypes. Then, make the elementType required. + */ + elementType?: T, +}>; + +export type NativeModuleStringTypeAnnotation = $ReadOnly<{ + type: 'StringTypeAnnotation', +}>; + +export type NativeModuleNumberTypeAnnotation = $ReadOnly<{ + type: 'NumberTypeAnnotation', +}>; + +export type NativeModuleInt32TypeAnnotation = $ReadOnly<{ + type: 'Int32TypeAnnotation', +}>; + +export type NativeModuleDoubleTypeAnnotation = $ReadOnly<{ + type: 'DoubleTypeAnnotation', +}>; + +export type NativeModuleFloatTypeAnnotation = $ReadOnly<{ + type: 'FloatTypeAnnotation', +}>; + +export type NativeModuleBooleanTypeAnnotation = $ReadOnly<{ + type: 'BooleanTypeAnnotation', +}>; + +export type NativeModuleEnumMembers = $ReadOnlyArray< + $ReadOnly<{ + name: string, + value: string, + }>, +>; + +export type NativeModuleEnumMemberType = + | 'NumberTypeAnnotation' + | 'StringTypeAnnotation'; + +export type NativeModuleEnumDeclaration = $ReadOnly<{ + name: string, + type: 'EnumDeclaration', + memberType: NativeModuleEnumMemberType, +}>; + +export type NativeModuleEnumDeclarationWithMembers = { + name: string, + type: 'EnumDeclarationWithMembers', + memberType: NativeModuleEnumMemberType, + members: NativeModuleEnumMembers, +}; + +export type NativeModuleGenericObjectTypeAnnotation = $ReadOnly<{ + type: 'GenericObjectTypeAnnotation', + // a dictionary type is codegen as "Object" + // but we know all its members are in the same type + // when it happens, the following field is non-null + dictionaryValueType?: Nullable, +}>; + +export type NativeModuleTypeAliasTypeAnnotation = $ReadOnly<{ + type: 'TypeAliasTypeAnnotation', + name: string, +}>; + +export type NativeModulePromiseTypeAnnotation = $ReadOnly<{ + type: 'PromiseTypeAnnotation', + elementType?: Nullable, +}>; + +export type UnionTypeAnnotationMemberType = + | 'NumberTypeAnnotation' + | 'ObjectTypeAnnotation' + | 'StringTypeAnnotation'; + +export type NativeModuleUnionTypeAnnotation = $ReadOnly<{ + type: 'UnionTypeAnnotation', + memberType: UnionTypeAnnotationMemberType, +}>; + +export type NativeModuleMixedTypeAnnotation = $ReadOnly<{ + type: 'MixedTypeAnnotation', +}>; + +export type NativeModuleBaseTypeAnnotation = + | NativeModuleStringTypeAnnotation + | NativeModuleNumberTypeAnnotation + | NativeModuleInt32TypeAnnotation + | NativeModuleDoubleTypeAnnotation + | NativeModuleFloatTypeAnnotation + | NativeModuleBooleanTypeAnnotation + | NativeModuleEnumDeclaration + | NativeModuleGenericObjectTypeAnnotation + | ReservedTypeAnnotation + | NativeModuleTypeAliasTypeAnnotation + | NativeModuleArrayTypeAnnotation> + | NativeModuleObjectTypeAnnotation + | NativeModuleUnionTypeAnnotation + | NativeModuleMixedTypeAnnotation; + +export type NativeModuleParamTypeAnnotation = + | NativeModuleBaseTypeAnnotation + | NativeModuleParamOnlyTypeAnnotation; + +export type NativeModuleReturnTypeAnnotation = + | NativeModuleBaseTypeAnnotation + | NativeModuleReturnOnlyTypeAnnotation; + +export type NativeModuleTypeAnnotation = + | NativeModuleBaseTypeAnnotation + | NativeModuleParamOnlyTypeAnnotation + | NativeModuleReturnOnlyTypeAnnotation; + +type NativeModuleParamOnlyTypeAnnotation = NativeModuleFunctionTypeAnnotation; + +type NativeModuleReturnOnlyTypeAnnotation = + | NativeModulePromiseTypeAnnotation + | VoidTypeAnnotation; diff --git a/packages/react-native-codegen/lib/SchemaValidator.d.ts b/packages/react-native-codegen/lib/SchemaValidator.d.ts new file mode 100644 index 00000000000000..7fb8125fa8fe63 --- /dev/null +++ b/packages/react-native-codegen/lib/SchemaValidator.d.ts @@ -0,0 +1,11 @@ +/** + * 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. + */ + +import type { SchemaType } from './CodegenSchema'; + +export declare function getErrors(schema: SchemaType): readonly string[]; +export declare function validate(schema: SchemaType): void; diff --git a/packages/react-native-codegen/lib/SchemaValidator.js b/packages/react-native-codegen/lib/SchemaValidator.js new file mode 100644 index 00000000000000..0a6367798555fd --- /dev/null +++ b/packages/react-native-codegen/lib/SchemaValidator.js @@ -0,0 +1,54 @@ +/** + * 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 nullthrows = require('nullthrows'); +function getErrors(schema) { + const errors = new Set(); + + // Map of component name -> Array of module names + const componentModules = new Map(); + Object.keys(schema.modules).forEach(moduleName => { + const module = schema.modules[moduleName]; + if (module.components == null) { + return; + } + Object.keys(module.components).forEach(componentName => { + if (module.components == null) { + return; + } + if (!componentModules.has(componentName)) { + componentModules.set(componentName, []); + } + nullthrows(componentModules.get(componentName)).push(moduleName); + }); + }); + componentModules.forEach((modules, componentName) => { + if (modules.length > 1) { + errors.add( + `Duplicate components found with name ${componentName}. Found in modules ${modules.join( + ', ', + )}`, + ); + } + }); + return Array.from(errors).sort(); +} +function validate(schema) { + const errors = getErrors(schema); + if (errors.length !== 0) { + throw new Error('Errors found validating schema:\n' + errors.join('\n')); + } +} +module.exports = { + getErrors, + validate, +}; diff --git a/packages/react-native-codegen/lib/SchemaValidator.js.flow b/packages/react-native-codegen/lib/SchemaValidator.js.flow new file mode 100644 index 00000000000000..dc59d9cd77b29f --- /dev/null +++ b/packages/react-native-codegen/lib/SchemaValidator.js.flow @@ -0,0 +1,67 @@ +/** + * 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-local + * @format + */ + +'use strict'; + +import type {SchemaType} from './CodegenSchema'; + +const nullthrows = require('nullthrows'); + +function getErrors(schema: SchemaType): $ReadOnlyArray { + const errors = new Set(); + + // Map of component name -> Array of module names + const componentModules: Map> = new Map(); + + Object.keys(schema.modules).forEach(moduleName => { + const module = schema.modules[moduleName]; + + if (module.components == null) { + return; + } + + Object.keys(module.components).forEach(componentName => { + if (module.components == null) { + return; + } + + if (!componentModules.has(componentName)) { + componentModules.set(componentName, []); + } + + nullthrows(componentModules.get(componentName)).push(moduleName); + }); + }); + + componentModules.forEach((modules, componentName) => { + if (modules.length > 1) { + errors.add( + `Duplicate components found with name ${componentName}. Found in modules ${modules.join( + ', ', + )}`, + ); + } + }); + + return Array.from(errors).sort(); +} + +function validate(schema: SchemaType) { + const errors = getErrors(schema); + + if (errors.length !== 0) { + throw new Error('Errors found validating schema:\n' + errors.join('\n')); + } +} + +module.exports = { + getErrors, + validate, +}; diff --git a/packages/react-native-codegen/lib/cli/combine/combine-js-to-schema-cli.js b/packages/react-native-codegen/lib/cli/combine/combine-js-to-schema-cli.js new file mode 100644 index 00000000000000..0c6f74abaabfa0 --- /dev/null +++ b/packages/react-native-codegen/lib/cli/combine/combine-js-to-schema-cli.js @@ -0,0 +1,81 @@ +/** + * 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 + * @oncall react_native + */ + +'use strict'; + +function _toArray(r) { + return ( + _arrayWithHoles(r) || + _iterableToArray(r) || + _unsupportedIterableToArray(r) || + _nonIterableRest() + ); +} +function _nonIterableRest() { + throw new TypeError( + 'Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.', + ); +} +function _unsupportedIterableToArray(r, a) { + if (r) { + if ('string' == typeof r) return _arrayLikeToArray(r, a); + var t = {}.toString.call(r).slice(8, -1); + return ( + 'Object' === t && r.constructor && (t = r.constructor.name), + 'Map' === t || 'Set' === t + ? Array.from(r) + : 'Arguments' === t || + /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) + ? _arrayLikeToArray(r, a) + : void 0 + ); + } +} +function _arrayLikeToArray(r, a) { + (null == a || a > r.length) && (a = r.length); + for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; + return n; +} +function _iterableToArray(r) { + if ( + ('undefined' != typeof Symbol && null != r[Symbol.iterator]) || + null != r['@@iterator'] + ) + return Array.from(r); +} +function _arrayWithHoles(r) { + if (Array.isArray(r)) return r; +} +const _require = require('./combine-js-to-schema'), + combineSchemasInFileListAndWriteToFile = + _require.combineSchemasInFileListAndWriteToFile; +const yargs = require('yargs'); +const argv = yargs + .option('p', { + alias: 'platform', + }) + .option('e', { + alias: 'exclude', + }) + .parseSync(); +const _argv$_ = _toArray(argv._), + outfile = _argv$_[0], + fileList = _argv$_.slice(1); +const platform = argv.platform; +const exclude = argv.exclude; +const excludeRegExp = + exclude != null && exclude !== '' ? new RegExp(exclude) : null; +combineSchemasInFileListAndWriteToFile( + fileList, + platform != null ? platform.toLowerCase() : platform, + outfile, + excludeRegExp, +); diff --git a/packages/react-native-codegen/lib/cli/combine/combine-js-to-schema-cli.js.flow b/packages/react-native-codegen/lib/cli/combine/combine-js-to-schema-cli.js.flow new file mode 100644 index 00000000000000..f2c98912efeba4 --- /dev/null +++ b/packages/react-native-codegen/lib/cli/combine/combine-js-to-schema-cli.js.flow @@ -0,0 +1,39 @@ +/** + * 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-local + * @format + * @oncall react_native + */ + +'use strict'; + +const { + combineSchemasInFileListAndWriteToFile, +} = require('./combine-js-to-schema'); +const yargs = require('yargs'); + +const argv = yargs + .option('p', { + alias: 'platform', + }) + .option('e', { + alias: 'exclude', + }) + .parseSync(); + +const [outfile, ...fileList] = argv._; +const platform: ?string = argv.platform; +const exclude: string = argv.exclude; +const excludeRegExp: ?RegExp = + exclude != null && exclude !== '' ? new RegExp(exclude) : null; + +combineSchemasInFileListAndWriteToFile( + fileList, + platform != null ? platform.toLowerCase() : platform, + outfile, + excludeRegExp, +); diff --git a/packages/react-native-codegen/lib/cli/combine/combine-js-to-schema.js b/packages/react-native-codegen/lib/cli/combine/combine-js-to-schema.js new file mode 100644 index 00000000000000..c5a105463976ed --- /dev/null +++ b/packages/react-native-codegen/lib/cli/combine/combine-js-to-schema.js @@ -0,0 +1,150 @@ +/** + * 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'; + +function ownKeys(e, r) { + var t = Object.keys(e); + if (Object.getOwnPropertySymbols) { + var o = Object.getOwnPropertySymbols(e); + r && + (o = o.filter(function (r) { + return Object.getOwnPropertyDescriptor(e, r).enumerable; + })), + t.push.apply(t, o); + } + return t; +} +function _objectSpread(e) { + for (var r = 1; r < arguments.length; r++) { + var t = null != arguments[r] ? arguments[r] : {}; + r % 2 + ? ownKeys(Object(t), !0).forEach(function (r) { + _defineProperty(e, r, t[r]); + }) + : Object.getOwnPropertyDescriptors + ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) + : ownKeys(Object(t)).forEach(function (r) { + Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); + }); + } + return e; +} +function _defineProperty(e, r, t) { + return ( + (r = _toPropertyKey(r)) in e + ? Object.defineProperty(e, r, { + value: t, + enumerable: !0, + configurable: !0, + writable: !0, + }) + : (e[r] = t), + e + ); +} +function _toPropertyKey(t) { + var i = _toPrimitive(t, 'string'); + return 'symbol' == typeof i ? i : i + ''; +} +function _toPrimitive(t, r) { + if ('object' != typeof t || !t) return t; + var e = t[Symbol.toPrimitive]; + if (void 0 !== e) { + var i = e.call(t, r || 'default'); + if ('object' != typeof i) return i; + throw new TypeError('@@toPrimitive must return a primitive value.'); + } + return ('string' === r ? String : Number)(t); +} +const _require = require('../../parsers/flow/parser'), + FlowParser = _require.FlowParser; +const _require2 = require('../../parsers/typescript/parser'), + TypeScriptParser = _require2.TypeScriptParser; +const _require3 = require('./combine-utils'), + filterJSFile = _require3.filterJSFile; +const fs = require('fs'); +const glob = require('glob'); +const path = require('path'); +const flowParser = new FlowParser(); +const typescriptParser = new TypeScriptParser(); +function combineSchemas(files) { + return files.reduce( + (merged, filename) => { + const contents = fs.readFileSync(filename, 'utf8'); + if ( + contents && + (/export\s+default\s+\(?codegenNativeComponent { + if (!fs.lstatSync(file).isDirectory()) { + return [file]; + } + const filePattern = path.sep === '\\' ? file.replace(/\\/g, '/') : file; + return glob.sync(`${filePattern}/**/*.{js,ts,tsx}`, { + nodir: true, + // TODO: This will remove the need of slash substitution above for Windows, + // but it requires glob@v9+; with the package currenlty relying on + // glob@7.1.1; and flow-typed repo not having definitions for glob@9+. + // windowsPathsNoEscape: true, + }); + }) + .filter(element => filterJSFile(element, platform, exclude)); +} +function combineSchemasInFileList(fileList, platform, exclude) { + const expandedFileList = expandDirectoriesIntoFiles( + fileList, + platform, + exclude, + ); + const combined = combineSchemas(expandedFileList); + if (Object.keys(combined.modules).length === 0) { + console.error( + 'No modules to process in combine-js-to-schema-cli. If this is unexpected, please check if you set up your NativeComponent correctly. See combine-js-to-schema.js for how codegen finds modules.', + ); + } + return combined; +} +function combineSchemasInFileListAndWriteToFile( + fileList, + platform, + outfile, + exclude, +) { + const combined = combineSchemasInFileList(fileList, platform, exclude); + const formattedSchema = JSON.stringify(combined, null, 2); + fs.writeFileSync(outfile, formattedSchema); +} +module.exports = { + combineSchemas, + combineSchemasInFileList, + combineSchemasInFileListAndWriteToFile, +}; diff --git a/packages/react-native-codegen/lib/cli/combine/combine-js-to-schema.js.flow b/packages/react-native-codegen/lib/cli/combine/combine-js-to-schema.js.flow new file mode 100644 index 00000000000000..108edf99d81319 --- /dev/null +++ b/packages/react-native-codegen/lib/cli/combine/combine-js-to-schema.js.flow @@ -0,0 +1,107 @@ +/** + * 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-local + * @format + */ + +'use strict'; +import type {SchemaType} from '../../CodegenSchema.js'; + +const {FlowParser} = require('../../parsers/flow/parser'); +const {TypeScriptParser} = require('../../parsers/typescript/parser'); +const {filterJSFile} = require('./combine-utils'); +const fs = require('fs'); +const glob = require('glob'); +const path = require('path'); + +const flowParser = new FlowParser(); +const typescriptParser = new TypeScriptParser(); + +function combineSchemas(files: Array): SchemaType { + return files.reduce( + (merged, filename) => { + const contents = fs.readFileSync(filename, 'utf8'); + + if ( + contents && + (/export\s+default\s+\(?codegenNativeComponent, + platform: ?string, + exclude: ?RegExp, +): Array { + return fileList + .flatMap(file => { + if (!fs.lstatSync(file).isDirectory()) { + return [file]; + } + const filePattern = path.sep === '\\' ? file.replace(/\\/g, '/') : file; + return glob.sync(`${filePattern}/**/*.{js,ts,tsx}`, { + nodir: true, + // TODO: This will remove the need of slash substitution above for Windows, + // but it requires glob@v9+; with the package currenlty relying on + // glob@7.1.1; and flow-typed repo not having definitions for glob@9+. + // windowsPathsNoEscape: true, + }); + }) + .filter(element => filterJSFile(element, platform, exclude)); +} + +function combineSchemasInFileList( + fileList: Array, + platform: ?string, + exclude: ?RegExp, +): SchemaType { + const expandedFileList = expandDirectoriesIntoFiles( + fileList, + platform, + exclude, + ); + const combined = combineSchemas(expandedFileList); + if (Object.keys(combined.modules).length === 0) { + console.error( + 'No modules to process in combine-js-to-schema-cli. If this is unexpected, please check if you set up your NativeComponent correctly. See combine-js-to-schema.js for how codegen finds modules.', + ); + } + return combined; +} + +function combineSchemasInFileListAndWriteToFile( + fileList: Array, + platform: ?string, + outfile: string, + exclude: ?RegExp, +): void { + const combined = combineSchemasInFileList(fileList, platform, exclude); + const formattedSchema = JSON.stringify(combined, null, 2); + fs.writeFileSync(outfile, formattedSchema); +} + +module.exports = { + combineSchemas, + combineSchemasInFileList, + combineSchemasInFileListAndWriteToFile, +}; diff --git a/packages/react-native-codegen/lib/cli/combine/combine-schemas-cli.js b/packages/react-native-codegen/lib/cli/combine/combine-schemas-cli.js new file mode 100644 index 00000000000000..0f392a3751ecbd --- /dev/null +++ b/packages/react-native-codegen/lib/cli/combine/combine-schemas-cli.js @@ -0,0 +1,77 @@ +/** + * 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 assert = require('assert'); +const fs = require('fs'); +const yargs = require('yargs'); +const argv = yargs + .option('p', { + alias: 'platform', + type: 'string', + demandOption: true, + }) + .option('o', { + alias: 'output', + }) + .option('s', { + alias: 'schema-query', + }) + .parseSync(); +const platform = argv.platform.toLowerCase(); +const output = argv.output; +const schemaQuery = argv.s; +if (!['ios', 'android'].includes(platform)) { + throw new Error(`Invalid platform ${platform}`); +} +if (!schemaQuery.startsWith('@')) { + throw new Error( + "The argument provided to --schema-query must be a filename that starts with '@'.", + ); +} +const schemaQueryOutputFile = schemaQuery.replace(/^@/, ''); +const schemaQueryOutput = fs.readFileSync(schemaQueryOutputFile, 'utf8'); +const schemaFiles = schemaQueryOutput.split(' '); +const modules = {}; +const specNameToFile = {}; +for (const file of schemaFiles) { + const schema = JSON.parse(fs.readFileSync(file, 'utf8')); + if (schema.modules) { + for (const specName in schema.modules) { + const module = schema.modules[specName]; + if (modules[specName]) { + assert.deepEqual( + module, + modules[specName], + `App contained two specs with the same file name '${specName}'. Schemas: ${specNameToFile[specName]}, ${file}. Please rename one of the specs.`, + ); + } + if ( + module.excludedPlatforms && + module.excludedPlatforms.indexOf(platform) >= 0 + ) { + continue; + } + modules[specName] = module; + specNameToFile[specName] = file; + } + } +} +fs.writeFileSync( + output, + JSON.stringify( + { + modules, + }, + null, + 2, + ), +); diff --git a/packages/react-native-codegen/lib/cli/combine/combine-schemas-cli.js.flow b/packages/react-native-codegen/lib/cli/combine/combine-schemas-cli.js.flow new file mode 100644 index 00000000000000..f7a7b6f6ef0a28 --- /dev/null +++ b/packages/react-native-codegen/lib/cli/combine/combine-schemas-cli.js.flow @@ -0,0 +1,87 @@ +/** + * 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-local + * @format + */ + +'use strict'; + +import type { + ComponentSchema, + NativeModuleSchema, + SchemaType, +} from '../../CodegenSchema.js'; + +const assert = require('assert'); +const fs = require('fs'); +const yargs = require('yargs'); + +const argv = yargs + .option('p', { + alias: 'platform', + type: 'string', + demandOption: true, + }) + .option('o', { + alias: 'output', + }) + .option('s', { + alias: 'schema-query', + }) + .parseSync(); + +const platform: string = argv.platform.toLowerCase(); +const output: string = argv.output; +const schemaQuery: string = argv.s; + +if (!['ios', 'android'].includes(platform)) { + throw new Error(`Invalid platform ${platform}`); +} + +if (!schemaQuery.startsWith('@')) { + throw new Error( + "The argument provided to --schema-query must be a filename that starts with '@'.", + ); +} + +const schemaQueryOutputFile = schemaQuery.replace(/^@/, ''); +const schemaQueryOutput = fs.readFileSync(schemaQueryOutputFile, 'utf8'); + +const schemaFiles = schemaQueryOutput.split(' '); +const modules: { + [hasteModuleName: string]: NativeModuleSchema | ComponentSchema, +} = {}; +const specNameToFile: {[hasteModuleName: string]: string} = {}; + +for (const file of schemaFiles) { + const schema: SchemaType = JSON.parse(fs.readFileSync(file, 'utf8')); + + if (schema.modules) { + for (const specName in schema.modules) { + const module = schema.modules[specName]; + if (modules[specName]) { + assert.deepEqual( + module, + modules[specName], + `App contained two specs with the same file name '${specName}'. Schemas: ${specNameToFile[specName]}, ${file}. Please rename one of the specs.`, + ); + } + + if ( + module.excludedPlatforms && + module.excludedPlatforms.indexOf(platform) >= 0 + ) { + continue; + } + + modules[specName] = module; + specNameToFile[specName] = file; + } + } +} + +fs.writeFileSync(output, JSON.stringify({modules}, null, 2)); diff --git a/packages/react-native-codegen/lib/cli/combine/combine-utils.js b/packages/react-native-codegen/lib/cli/combine/combine-utils.js new file mode 100644 index 00000000000000..75b75f935d9207 --- /dev/null +++ b/packages/react-native-codegen/lib/cli/combine/combine-utils.js @@ -0,0 +1,56 @@ +/** + * 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 + * @oncall react_native + */ + +'use strict'; + +const path = require('path'); + +/** + * This function is used by the CLI to decide whether a JS/TS file has to be processed or not by the Codegen. + * Parameters: + * - file: the path to the file + * - currentPlatform: the current platform for which we are creating the specs + * Returns: `true` if the file can be used to generate some code; `false` otherwise + * + */ +function filterJSFile(file, currentPlatform, excludeRegExp) { + const isSpecFile = /^(Native.+|.+NativeComponent)/.test(path.basename(file)); + const isNotNativeUIManager = !file.endsWith('NativeUIManager.js'); + const isNotTest = !file.includes('__tests'); + const isNotExcluded = excludeRegExp == null || !excludeRegExp.test(file); + const isNotTSTypeDefinition = !file.endsWith('.d.ts'); + const isValidCandidate = + isSpecFile && + isNotNativeUIManager && + isNotExcluded && + isNotTest && + isNotTSTypeDefinition; + const filenameComponents = path.basename(file).split('.'); + const isPlatformAgnostic = filenameComponents.length === 2; + if (currentPlatform == null) { + // need to accept only files that are platform agnostic + return isValidCandidate && isPlatformAgnostic; + } + + // If a platform is passed, accept both platform agnostic specs... + if (isPlatformAgnostic) { + return isValidCandidate; + } + + // ...and specs that share the same platform as the one passed. + // specfiles must follow the pattern: [.].(js|ts|tsx) + const filePlatform = + filenameComponents.length > 2 ? filenameComponents[1] : 'unknown'; + return isValidCandidate && currentPlatform === filePlatform; +} +module.exports = { + filterJSFile, +}; diff --git a/packages/react-native-codegen/lib/cli/combine/combine-utils.js.flow b/packages/react-native-codegen/lib/cli/combine/combine-utils.js.flow new file mode 100644 index 00000000000000..8f120fb687f6ba --- /dev/null +++ b/packages/react-native-codegen/lib/cli/combine/combine-utils.js.flow @@ -0,0 +1,64 @@ +/** + * 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-local + * @format + * @oncall react_native + */ + +'use strict'; + +const path = require('path'); + +/** + * This function is used by the CLI to decide whether a JS/TS file has to be processed or not by the Codegen. + * Parameters: + * - file: the path to the file + * - currentPlatform: the current platform for which we are creating the specs + * Returns: `true` if the file can be used to generate some code; `false` otherwise + * + */ +function filterJSFile( + file: string, + currentPlatform: ?string, + excludeRegExp: ?RegExp, +): boolean { + const isSpecFile = /^(Native.+|.+NativeComponent)/.test(path.basename(file)); + const isNotNativeUIManager = !file.endsWith('NativeUIManager.js'); + const isNotTest = !file.includes('__tests'); + const isNotExcluded = excludeRegExp == null || !excludeRegExp.test(file); + const isNotTSTypeDefinition = !file.endsWith('.d.ts'); + + const isValidCandidate = + isSpecFile && + isNotNativeUIManager && + isNotExcluded && + isNotTest && + isNotTSTypeDefinition; + + const filenameComponents = path.basename(file).split('.'); + const isPlatformAgnostic = filenameComponents.length === 2; + + if (currentPlatform == null) { + // need to accept only files that are platform agnostic + return isValidCandidate && isPlatformAgnostic; + } + + // If a platform is passed, accept both platform agnostic specs... + if (isPlatformAgnostic) { + return isValidCandidate; + } + + // ...and specs that share the same platform as the one passed. + // specfiles must follow the pattern: [.].(js|ts|tsx) + const filePlatform = + filenameComponents.length > 2 ? filenameComponents[1] : 'unknown'; + return isValidCandidate && currentPlatform === filePlatform; +} + +module.exports = { + filterJSFile, +}; diff --git a/packages/react-native-codegen/lib/cli/generators/generate-all.js b/packages/react-native-codegen/lib/cli/generators/generate-all.js new file mode 100644 index 00000000000000..5cee549d4b249b --- /dev/null +++ b/packages/react-native-codegen/lib/cli/generators/generate-all.js @@ -0,0 +1,65 @@ +/** + * 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 + */ + +/** + * This generates all possible outputs by executing all available generators. + */ + +'use strict'; + +const RNCodegen = require('../../generators/RNCodegen.js'); +const fs = require('fs'); +const mkdirp = require('mkdirp'); +const args = process.argv.slice(2); +if (args.length < 3) { + throw new Error( + `Expected to receive path to schema, library name, output directory and module spec name. Received ${args.join( + ', ', + )}`, + ); +} +const schemaPath = args[0]; +const libraryName = args[1]; +const outputDirectory = args[2]; +const packageName = args[3]; +const assumeNonnull = args[4] === 'true' || args[4] === 'True'; +const schemaText = fs.readFileSync(schemaPath, 'utf-8'); +if (schemaText == null) { + throw new Error(`Can't find schema at ${schemaPath}`); +} +mkdirp.sync(outputDirectory); +let schema; +try { + schema = JSON.parse(schemaText); +} catch (err) { + throw new Error(`Can't parse schema to JSON. ${schemaPath}`); +} +RNCodegen.generate( + { + libraryName, + schema, + outputDirectory, + packageName, + assumeNonnull, + }, + { + generators: [ + 'descriptors', + 'events', + 'props', + 'states', + 'tests', + 'shadow-nodes', + 'modulesAndroid', + 'modulesCxx', + 'modulesIOS', + ], + }, +); diff --git a/packages/react-native-codegen/lib/cli/generators/generate-all.js.flow b/packages/react-native-codegen/lib/cli/generators/generate-all.js.flow new file mode 100644 index 00000000000000..46d20751020885 --- /dev/null +++ b/packages/react-native-codegen/lib/cli/generators/generate-all.js.flow @@ -0,0 +1,66 @@ +/** + * 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 + * @format + */ + +/** + * This generates all possible outputs by executing all available generators. + */ + +'use strict'; + +const RNCodegen = require('../../generators/RNCodegen.js'); +const fs = require('fs'); +const mkdirp = require('mkdirp'); + +const args = process.argv.slice(2); +if (args.length < 3) { + throw new Error( + `Expected to receive path to schema, library name, output directory and module spec name. Received ${args.join( + ', ', + )}`, + ); +} + +const schemaPath = args[0]; +const libraryName = args[1]; +const outputDirectory = args[2]; +const packageName = args[3]; +const assumeNonnull = args[4] === 'true' || args[4] === 'True'; + +const schemaText = fs.readFileSync(schemaPath, 'utf-8'); + +if (schemaText == null) { + throw new Error(`Can't find schema at ${schemaPath}`); +} + +mkdirp.sync(outputDirectory); + +let schema; +try { + schema = JSON.parse(schemaText); +} catch (err) { + throw new Error(`Can't parse schema to JSON. ${schemaPath}`); +} + +RNCodegen.generate( + {libraryName, schema, outputDirectory, packageName, assumeNonnull}, + { + generators: [ + 'descriptors', + 'events', + 'props', + 'states', + 'tests', + 'shadow-nodes', + 'modulesAndroid', + 'modulesCxx', + 'modulesIOS', + ], + }, +); diff --git a/packages/react-native-codegen/lib/cli/parser/parser-cli.js b/packages/react-native-codegen/lib/cli/parser/parser-cli.js new file mode 100644 index 00000000000000..cac41e6d284b31 --- /dev/null +++ b/packages/react-native-codegen/lib/cli/parser/parser-cli.js @@ -0,0 +1,61 @@ +/** + * 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 + * @oncall react_native + */ + +'use strict'; + +function _toArray(r) { + return ( + _arrayWithHoles(r) || + _iterableToArray(r) || + _unsupportedIterableToArray(r) || + _nonIterableRest() + ); +} +function _nonIterableRest() { + throw new TypeError( + 'Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.', + ); +} +function _unsupportedIterableToArray(r, a) { + if (r) { + if ('string' == typeof r) return _arrayLikeToArray(r, a); + var t = {}.toString.call(r).slice(8, -1); + return ( + 'Object' === t && r.constructor && (t = r.constructor.name), + 'Map' === t || 'Set' === t + ? Array.from(r) + : 'Arguments' === t || + /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) + ? _arrayLikeToArray(r, a) + : void 0 + ); + } +} +function _arrayLikeToArray(r, a) { + (null == a || a > r.length) && (a = r.length); + for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; + return n; +} +function _iterableToArray(r) { + if ( + ('undefined' != typeof Symbol && null != r[Symbol.iterator]) || + null != r['@@iterator'] + ) + return Array.from(r); +} +function _arrayWithHoles(r) { + if (Array.isArray(r)) return r; +} +const parseFiles = require('./parser.js'); +const _process$argv$slice = process.argv.slice(2), + _process$argv$slice2 = _toArray(_process$argv$slice), + fileList = _process$argv$slice2.slice(0); +parseFiles(fileList); diff --git a/packages/react-native-codegen/lib/cli/parser/parser-cli.js.flow b/packages/react-native-codegen/lib/cli/parser/parser-cli.js.flow new file mode 100644 index 00000000000000..084b11c7adec1a --- /dev/null +++ b/packages/react-native-codegen/lib/cli/parser/parser-cli.js.flow @@ -0,0 +1,18 @@ +/** + * 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-local + * @format + * @oncall react_native + */ + +'use strict'; + +const parseFiles = require('./parser.js'); + +const [...fileList] = process.argv.slice(2); + +parseFiles(fileList); diff --git a/packages/react-native-codegen/lib/cli/parser/parser.js b/packages/react-native-codegen/lib/cli/parser/parser.js new file mode 100644 index 00000000000000..45179701be3028 --- /dev/null +++ b/packages/react-native-codegen/lib/cli/parser/parser.js @@ -0,0 +1,28 @@ +/** + * 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 _require = require('../../parsers/flow/parser'), + FlowParser = _require.FlowParser; +const _require2 = require('../../parsers/typescript/parser'), + TypeScriptParser = _require2.TypeScriptParser; +const path = require('path'); +const flowParser = new FlowParser(); +const typescriptParser = new TypeScriptParser(); +function parseFiles(files) { + files.forEach(filename => { + const isTypeScript = + path.extname(filename) === '.ts' || path.extname(filename) === '.tsx'; + const parser = isTypeScript ? typescriptParser : flowParser; + console.log(filename, JSON.stringify(parser.parseFile(filename), null, 2)); + }); +} +module.exports = parseFiles; diff --git a/packages/react-native-codegen/lib/cli/parser/parser.js.flow b/packages/react-native-codegen/lib/cli/parser/parser.js.flow new file mode 100644 index 00000000000000..850e27fff4c846 --- /dev/null +++ b/packages/react-native-codegen/lib/cli/parser/parser.js.flow @@ -0,0 +1,31 @@ +/** + * 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-local + * @format + */ + +'use strict'; + +const {FlowParser} = require('../../parsers/flow/parser'); +const {TypeScriptParser} = require('../../parsers/typescript/parser'); +const path = require('path'); + +const flowParser = new FlowParser(); +const typescriptParser = new TypeScriptParser(); + +function parseFiles(files: Array) { + files.forEach(filename => { + const isTypeScript = + path.extname(filename) === '.ts' || path.extname(filename) === '.tsx'; + + const parser = isTypeScript ? typescriptParser : flowParser; + + console.log(filename, JSON.stringify(parser.parseFile(filename), null, 2)); + }); +} + +module.exports = parseFiles; diff --git a/packages/react-native-codegen/lib/cli/parser/parser.sh b/packages/react-native-codegen/lib/cli/parser/parser.sh new file mode 100644 index 00000000000000..8d130f250017be --- /dev/null +++ b/packages/react-native-codegen/lib/cli/parser/parser.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# 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. + +set -e +set -u + +THIS_DIR=$(cd -P "$(dirname "$(realpath "${BASH_SOURCE[0]}" || echo "${BASH_SOURCE[0]}")")" && pwd) + +# shellcheck source=xplat/js/env-utils/setup_env_vars.sh +source "$THIS_DIR/../../../../../../env-utils/setup_env_vars.sh" + +exec "$FLOW_NODE_BINARY" "$THIS_DIR/parser.js" "$@" diff --git a/packages/react-native-codegen/lib/generators/RNCodegen.d.ts b/packages/react-native-codegen/lib/generators/RNCodegen.d.ts new file mode 100644 index 00000000000000..f73eb2e0fa51b3 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/RNCodegen.d.ts @@ -0,0 +1,99 @@ +/** + * 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. + */ + +import type { SchemaType } from '../CodegenSchema'; + +export type FilesOutput = Map; +export type LibraryGeneratorFunction = (libraryName: string, schema: SchemaType, packageName: string | undefined, assumeNonnull: boolean) => FilesOutput; +export type SchemaGeneratorFunction = (schemas: { [key: string]: SchemaType }) => FilesOutput; +export type ViewGeneratorFunction = (libraryName: string, schema: SchemaType) => FilesOutput; + +type LibraryGeneratorNames = + | 'generateComponentDescriptorH' + | 'generateComponentDescriptorCpp' + | 'generateComponentHObjCpp' + | 'generateEventEmitterCpp' + | 'generateEventEmitterH' + | 'generatePropsCpp' + | 'generatePropsH' + | 'generateStateCpp' + | 'generateStateH' + | 'generateModuleH' + | 'generateModuleCpp' + | 'generateModuleObjCpp' + | 'generateModuleJavaSpec' + | 'generateModuleJniCpp' + | 'generateModuleJniH' + | 'generatePropsJavaInterface' + | 'generatePropsJavaDelegate' + | 'generateTests' + | 'generateShadowNodeCpp' + | 'generateShadowNodeH' + ; + +type SchemaGeneratorNames = + | 'generateThirdPartyFabricComponentsProviderObjCpp' + | 'generateThirdPartyFabricComponentsProviderH' + ; + +type ViewGeneratorNames = + | 'generateViewConfigJs' + ; + +export type AllGenerators = + & { readonly [key in LibraryGeneratorNames]: LibraryGeneratorFunction; } + & { readonly [key in SchemaGeneratorNames]: SchemaGeneratorFunction; } + & { readonly [key in ViewGeneratorNames]: ViewGeneratorFunction; } + ; + +export type LibraryGenerators = + | 'componentsAndroid' + | 'componentsIOS' + | 'descriptors' + | 'events' + | 'props' + | 'states' + | 'tests' + | 'shadow-nodes' + | 'modulesAndroid' + | 'modulesCxx' + | 'modulesIOS' + ; + +export type SchemaGenerators = + | 'providerIOS' + ; + +export interface LibraryOptions { + libraryName: string; + schema: SchemaType; + outputDirectory: string; + packageName?: string | undefined; + assumeNonnull: boolean; +} + +export interface LibraryConfig { + generators: LibraryGenerators[]; + test?: boolean | undefined; +} + +export interface SchemasOptions { + schemas: { [key: string]: SchemaType }; + outputDirectory: string; +} + +export interface SchemasConfig { + generators: SchemaGenerators[]; + test?: boolean | undefined; +} + +export declare const allGenerators: AllGenerators; +export declare const libraryGenerators: { readonly [key in LibraryGenerators]: LibraryGeneratorFunction }; +export declare const schemaGenerators: { readonly [key in SchemaGenerators]: SchemaGeneratorFunction }; +export declare function generate(options: LibraryOptions, config: LibraryConfig): boolean; +export declare function generateFromSchemas(options: SchemasOptions, config: SchemasConfig): boolean; +export declare function generateViewConfig(options: LibraryOptions): string; diff --git a/packages/react-native-codegen/lib/generators/RNCodegen.js b/packages/react-native-codegen/lib/generators/RNCodegen.js new file mode 100644 index 00000000000000..69f28cff8ac064 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/RNCodegen.js @@ -0,0 +1,263 @@ +/** + * 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'; + +/* +TODO: + +- ViewConfigs should spread in View's valid attributes +*/ +const schemaValidator = require('../SchemaValidator.js'); +const generateComponentDescriptorCpp = require('./components/GenerateComponentDescriptorCpp.js'); +const generateComponentDescriptorH = require('./components/GenerateComponentDescriptorH.js'); +const generateComponentHObjCpp = require('./components/GenerateComponentHObjCpp.js'); +const generateEventEmitterCpp = require('./components/GenerateEventEmitterCpp.js'); +const generateEventEmitterH = require('./components/GenerateEventEmitterH.js'); +const generatePropsCpp = require('./components/GeneratePropsCpp.js'); +const generatePropsH = require('./components/GeneratePropsH.js'); +const generatePropsJavaDelegate = require('./components/GeneratePropsJavaDelegate.js'); +const generatePropsJavaInterface = require('./components/GeneratePropsJavaInterface.js'); +const generateShadowNodeCpp = require('./components/GenerateShadowNodeCpp.js'); +const generateShadowNodeH = require('./components/GenerateShadowNodeH.js'); +const generateStateCpp = require('./components/GenerateStateCpp.js'); +const generateStateH = require('./components/GenerateStateH.js'); +const generateTests = require('./components/GenerateTests.js'); +const generateThirdPartyFabricComponentsProviderH = require('./components/GenerateThirdPartyFabricComponentsProviderH.js'); +const generateThirdPartyFabricComponentsProviderObjCpp = require('./components/GenerateThirdPartyFabricComponentsProviderObjCpp.js'); +const generateViewConfigJs = require('./components/GenerateViewConfigJs.js'); +const generateModuleCpp = require('./modules/GenerateModuleCpp.js'); +const generateModuleH = require('./modules/GenerateModuleH.js'); +const generateModuleJavaSpec = require('./modules/GenerateModuleJavaSpec.js'); +const generateModuleJniCpp = require('./modules/GenerateModuleJniCpp.js'); +const generateModuleJniH = require('./modules/GenerateModuleJniH.js'); +const generateModuleObjCpp = require('./modules/GenerateModuleObjCpp'); +const fs = require('fs'); +const path = require('path'); +const ALL_GENERATORS = { + generateComponentDescriptorH: generateComponentDescriptorH.generate, + generateComponentDescriptorCpp: generateComponentDescriptorCpp.generate, + generateComponentHObjCpp: generateComponentHObjCpp.generate, + generateEventEmitterCpp: generateEventEmitterCpp.generate, + generateEventEmitterH: generateEventEmitterH.generate, + generatePropsCpp: generatePropsCpp.generate, + generatePropsH: generatePropsH.generate, + generateStateCpp: generateStateCpp.generate, + generateStateH: generateStateH.generate, + generateModuleH: generateModuleH.generate, + generateModuleCpp: generateModuleCpp.generate, + generateModuleObjCpp: generateModuleObjCpp.generate, + generateModuleJavaSpec: generateModuleJavaSpec.generate, + generateModuleJniCpp: generateModuleJniCpp.generate, + generateModuleJniH: generateModuleJniH.generate, + generatePropsJavaInterface: generatePropsJavaInterface.generate, + generatePropsJavaDelegate: generatePropsJavaDelegate.generate, + generateTests: generateTests.generate, + generateShadowNodeCpp: generateShadowNodeCpp.generate, + generateShadowNodeH: generateShadowNodeH.generate, + generateThirdPartyFabricComponentsProviderObjCpp: + generateThirdPartyFabricComponentsProviderObjCpp.generate, + generateThirdPartyFabricComponentsProviderH: + generateThirdPartyFabricComponentsProviderH.generate, + generateViewConfigJs: generateViewConfigJs.generate, +}; +const LIBRARY_GENERATORS = { + descriptors: [generateComponentDescriptorH.generate], + events: [generateEventEmitterCpp.generate, generateEventEmitterH.generate], + states: [generateStateCpp.generate, generateStateH.generate], + props: [ + generateComponentHObjCpp.generate, + generatePropsCpp.generate, + generatePropsH.generate, + generatePropsJavaInterface.generate, + generatePropsJavaDelegate.generate, + ], + // TODO: Refactor this to consolidate various C++ output variation instead of forking per platform. + componentsAndroid: [ + // JNI/C++ files + generateComponentDescriptorH.generate, + generateComponentDescriptorCpp.generate, + generateEventEmitterCpp.generate, + generateEventEmitterH.generate, + generatePropsCpp.generate, + generatePropsH.generate, + generateStateCpp.generate, + generateStateH.generate, + generateShadowNodeCpp.generate, + generateShadowNodeH.generate, + // Java files + generatePropsJavaInterface.generate, + generatePropsJavaDelegate.generate, + ], + componentsIOS: [ + generateComponentDescriptorH.generate, + generateComponentDescriptorCpp.generate, + generateEventEmitterCpp.generate, + generateEventEmitterH.generate, + generateComponentHObjCpp.generate, + generatePropsCpp.generate, + generatePropsH.generate, + generateStateCpp.generate, + generateStateH.generate, + generateShadowNodeCpp.generate, + generateShadowNodeH.generate, + ], + modulesAndroid: [ + generateModuleJniCpp.generate, + generateModuleJniH.generate, + generateModuleJavaSpec.generate, + ], + modulesCxx: [generateModuleCpp.generate, generateModuleH.generate], + modulesIOS: [generateModuleObjCpp.generate], + tests: [generateTests.generate], + 'shadow-nodes': [ + generateShadowNodeCpp.generate, + generateShadowNodeH.generate, + ], +}; +const SCHEMAS_GENERATORS = { + providerIOS: [ + generateThirdPartyFabricComponentsProviderObjCpp.generate, + generateThirdPartyFabricComponentsProviderH.generate, + ], +}; +function writeMapToFiles(map) { + let success = true; + map.forEach(file => { + try { + const location = path.join(file.outputDir, file.name); + const dirName = path.dirname(location); + if (!fs.existsSync(dirName)) { + fs.mkdirSync(dirName, { + recursive: true, + }); + } + fs.writeFileSync(location, file.content); + } catch (error) { + success = false; + console.error(`Failed to write ${file.name} to ${file.outputDir}`, error); + } + }); + return success; +} +function checkFilesForChanges(generated) { + let hasChanged = false; + generated.forEach(file => { + const location = path.join(file.outputDir, file.name); + const currentContents = fs.readFileSync(location, 'utf8'); + if (currentContents !== file.content) { + console.error(`- ${file.name} has changed`); + hasChanged = true; + } + }); + return !hasChanged; +} +function checkOrWriteFiles(generatedFiles, test) { + if (test === true) { + return checkFilesForChanges(generatedFiles); + } + return writeMapToFiles(generatedFiles); +} +module.exports = { + allGenerators: ALL_GENERATORS, + libraryGenerators: LIBRARY_GENERATORS, + schemaGenerators: SCHEMAS_GENERATORS, + generate( + { + libraryName, + schema, + outputDirectory, + packageName, + assumeNonnull, + useLocalIncludePaths, + }, + {generators, test}, + ) { + schemaValidator.validate(schema); + const defaultHeaderPrefix = 'react/renderer/components'; + const headerPrefix = + useLocalIncludePaths === true + ? '' + : `${defaultHeaderPrefix}/${libraryName}/`; + function composePath(intermediate) { + return path.join(outputDirectory, intermediate, libraryName); + } + const componentIOSOutput = composePath( + useLocalIncludePaths === true ? '' : defaultHeaderPrefix, + ); + const modulesIOSOutput = composePath('./'); + const outputFoldersForGenerators = { + componentsIOS: componentIOSOutput, + modulesIOS: modulesIOSOutput, + descriptors: outputDirectory, + events: outputDirectory, + props: outputDirectory, + states: outputDirectory, + componentsAndroid: outputDirectory, + modulesAndroid: outputDirectory, + modulesCxx: outputDirectory, + tests: outputDirectory, + 'shadow-nodes': outputDirectory, + }; + const generatedFiles = []; + for (const name of generators) { + for (const generator of LIBRARY_GENERATORS[name]) { + generator( + libraryName, + schema, + packageName, + assumeNonnull, + headerPrefix, + ).forEach((contents, fileName) => { + generatedFiles.push({ + name: fileName, + content: contents, + outputDir: outputFoldersForGenerators[name], + }); + }); + } + } + return checkOrWriteFiles(generatedFiles, test); + }, + generateFromSchemas( + {schemas, outputDirectory, supportedApplePlatforms}, + {generators, test}, + ) { + Object.keys(schemas).forEach(libraryName => + schemaValidator.validate(schemas[libraryName]), + ); + const generatedFiles = []; + for (const name of generators) { + for (const generator of SCHEMAS_GENERATORS[name]) { + generator(schemas, supportedApplePlatforms).forEach( + (contents, fileName) => { + generatedFiles.push({ + name: fileName, + content: contents, + outputDir: outputDirectory, + }); + }, + ); + } + } + return checkOrWriteFiles(generatedFiles, test); + }, + generateViewConfig({libraryName, schema}) { + schemaValidator.validate(schema); + const result = generateViewConfigJs + .generate(libraryName, schema) + .values() + .next(); + if (typeof result.value !== 'string') { + throw new Error(`Failed to generate view config for ${libraryName}`); + } + return result.value; + }, +}; diff --git a/packages/react-native-codegen/lib/generators/RNCodegen.js.flow b/packages/react-native-codegen/lib/generators/RNCodegen.js.flow new file mode 100644 index 00000000000000..d363ceb0957c1c --- /dev/null +++ b/packages/react-native-codegen/lib/generators/RNCodegen.js.flow @@ -0,0 +1,335 @@ +/** + * 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-local + * @format + */ + +'use strict'; + +/* +TODO: + +- ViewConfigs should spread in View's valid attributes +*/ + +import type {SchemaType} from '../CodegenSchema'; + +const schemaValidator = require('../SchemaValidator.js'); +const generateComponentDescriptorCpp = require('./components/GenerateComponentDescriptorCpp.js'); +const generateComponentDescriptorH = require('./components/GenerateComponentDescriptorH.js'); +const generateComponentHObjCpp = require('./components/GenerateComponentHObjCpp.js'); +const generateEventEmitterCpp = require('./components/GenerateEventEmitterCpp.js'); +const generateEventEmitterH = require('./components/GenerateEventEmitterH.js'); +const generatePropsCpp = require('./components/GeneratePropsCpp.js'); +const generatePropsH = require('./components/GeneratePropsH.js'); +const generatePropsJavaDelegate = require('./components/GeneratePropsJavaDelegate.js'); +const generatePropsJavaInterface = require('./components/GeneratePropsJavaInterface.js'); +const generateShadowNodeCpp = require('./components/GenerateShadowNodeCpp.js'); +const generateShadowNodeH = require('./components/GenerateShadowNodeH.js'); +const generateStateCpp = require('./components/GenerateStateCpp.js'); +const generateStateH = require('./components/GenerateStateH.js'); +const generateTests = require('./components/GenerateTests.js'); +const generateThirdPartyFabricComponentsProviderH = require('./components/GenerateThirdPartyFabricComponentsProviderH.js'); +const generateThirdPartyFabricComponentsProviderObjCpp = require('./components/GenerateThirdPartyFabricComponentsProviderObjCpp.js'); +const generateViewConfigJs = require('./components/GenerateViewConfigJs.js'); +const generateModuleCpp = require('./modules/GenerateModuleCpp.js'); +const generateModuleH = require('./modules/GenerateModuleH.js'); +const generateModuleJavaSpec = require('./modules/GenerateModuleJavaSpec.js'); +const generateModuleJniCpp = require('./modules/GenerateModuleJniCpp.js'); +const generateModuleJniH = require('./modules/GenerateModuleJniH.js'); +const generateModuleObjCpp = require('./modules/GenerateModuleObjCpp'); +const fs = require('fs'); +const path = require('path'); + +const ALL_GENERATORS = { + generateComponentDescriptorH: generateComponentDescriptorH.generate, + generateComponentDescriptorCpp: generateComponentDescriptorCpp.generate, + generateComponentHObjCpp: generateComponentHObjCpp.generate, + generateEventEmitterCpp: generateEventEmitterCpp.generate, + generateEventEmitterH: generateEventEmitterH.generate, + generatePropsCpp: generatePropsCpp.generate, + generatePropsH: generatePropsH.generate, + generateStateCpp: generateStateCpp.generate, + generateStateH: generateStateH.generate, + generateModuleH: generateModuleH.generate, + generateModuleCpp: generateModuleCpp.generate, + generateModuleObjCpp: generateModuleObjCpp.generate, + generateModuleJavaSpec: generateModuleJavaSpec.generate, + generateModuleJniCpp: generateModuleJniCpp.generate, + generateModuleJniH: generateModuleJniH.generate, + generatePropsJavaInterface: generatePropsJavaInterface.generate, + generatePropsJavaDelegate: generatePropsJavaDelegate.generate, + generateTests: generateTests.generate, + generateShadowNodeCpp: generateShadowNodeCpp.generate, + generateShadowNodeH: generateShadowNodeH.generate, + generateThirdPartyFabricComponentsProviderObjCpp: + generateThirdPartyFabricComponentsProviderObjCpp.generate, + generateThirdPartyFabricComponentsProviderH: + generateThirdPartyFabricComponentsProviderH.generate, + generateViewConfigJs: generateViewConfigJs.generate, +}; + +type LibraryOptions = $ReadOnly<{ + libraryName: string, + schema: SchemaType, + outputDirectory: string, + packageName?: string, // Some platforms have a notion of package, which should be configurable. + assumeNonnull: boolean, + useLocalIncludePaths?: boolean, +}>; + +type SchemasOptions = $ReadOnly<{ + schemas: {[string]: SchemaType}, + outputDirectory: string, + supportedApplePlatforms?: {[string]: {[string]: boolean}}, +}>; + +type LibraryGenerators = + | 'componentsAndroid' + | 'componentsIOS' + | 'descriptors' + | 'events' + | 'props' + | 'states' + | 'tests' + | 'shadow-nodes' + | 'modulesAndroid' + | 'modulesCxx' + | 'modulesIOS'; + +type SchemasGenerators = 'providerIOS'; + +type LibraryConfig = $ReadOnly<{ + generators: Array, + test?: boolean, +}>; + +type SchemasConfig = $ReadOnly<{ + generators: Array, + test?: boolean, +}>; + +const LIBRARY_GENERATORS = { + descriptors: [generateComponentDescriptorH.generate], + events: [generateEventEmitterCpp.generate, generateEventEmitterH.generate], + states: [generateStateCpp.generate, generateStateH.generate], + props: [ + generateComponentHObjCpp.generate, + generatePropsCpp.generate, + generatePropsH.generate, + generatePropsJavaInterface.generate, + generatePropsJavaDelegate.generate, + ], + // TODO: Refactor this to consolidate various C++ output variation instead of forking per platform. + componentsAndroid: [ + // JNI/C++ files + generateComponentDescriptorH.generate, + generateComponentDescriptorCpp.generate, + generateEventEmitterCpp.generate, + generateEventEmitterH.generate, + generatePropsCpp.generate, + generatePropsH.generate, + generateStateCpp.generate, + generateStateH.generate, + generateShadowNodeCpp.generate, + generateShadowNodeH.generate, + // Java files + generatePropsJavaInterface.generate, + generatePropsJavaDelegate.generate, + ], + componentsIOS: [ + generateComponentDescriptorH.generate, + generateComponentDescriptorCpp.generate, + generateEventEmitterCpp.generate, + generateEventEmitterH.generate, + generateComponentHObjCpp.generate, + generatePropsCpp.generate, + generatePropsH.generate, + generateStateCpp.generate, + generateStateH.generate, + generateShadowNodeCpp.generate, + generateShadowNodeH.generate, + ], + modulesAndroid: [ + generateModuleJniCpp.generate, + generateModuleJniH.generate, + generateModuleJavaSpec.generate, + ], + modulesCxx: [generateModuleCpp.generate, generateModuleH.generate], + modulesIOS: [generateModuleObjCpp.generate], + tests: [generateTests.generate], + 'shadow-nodes': [ + generateShadowNodeCpp.generate, + generateShadowNodeH.generate, + ], +}; + +const SCHEMAS_GENERATORS = { + providerIOS: [ + generateThirdPartyFabricComponentsProviderObjCpp.generate, + generateThirdPartyFabricComponentsProviderH.generate, + ], +}; + +type CodeGenFile = { + name: string, + content: string, + outputDir: string, +}; + +function writeMapToFiles(map: Array) { + let success = true; + map.forEach(file => { + try { + const location = path.join(file.outputDir, file.name); + const dirName = path.dirname(location); + if (!fs.existsSync(dirName)) { + fs.mkdirSync(dirName, {recursive: true}); + } + fs.writeFileSync(location, file.content); + } catch (error) { + success = false; + console.error(`Failed to write ${file.name} to ${file.outputDir}`, error); + } + }); + + return success; +} + +function checkFilesForChanges(generated: Array): boolean { + let hasChanged = false; + + generated.forEach(file => { + const location = path.join(file.outputDir, file.name); + const currentContents = fs.readFileSync(location, 'utf8'); + if (currentContents !== file.content) { + console.error(`- ${file.name} has changed`); + + hasChanged = true; + } + }); + + return !hasChanged; +} + +function checkOrWriteFiles( + generatedFiles: Array, + test: void | boolean, +): boolean { + if (test === true) { + return checkFilesForChanges(generatedFiles); + } + return writeMapToFiles(generatedFiles); +} + +module.exports = { + allGenerators: ALL_GENERATORS, + libraryGenerators: LIBRARY_GENERATORS, + schemaGenerators: SCHEMAS_GENERATORS, + + generate( + { + libraryName, + schema, + outputDirectory, + packageName, + assumeNonnull, + useLocalIncludePaths, + }: LibraryOptions, + {generators, test}: LibraryConfig, + ): boolean { + schemaValidator.validate(schema); + + const defaultHeaderPrefix = 'react/renderer/components'; + const headerPrefix = + useLocalIncludePaths === true + ? '' + : `${defaultHeaderPrefix}/${libraryName}/`; + function composePath(intermediate: string) { + return path.join(outputDirectory, intermediate, libraryName); + } + + const componentIOSOutput = composePath( + useLocalIncludePaths === true ? '' : defaultHeaderPrefix, + ); + const modulesIOSOutput = composePath('./'); + + const outputFoldersForGenerators = { + componentsIOS: componentIOSOutput, + modulesIOS: modulesIOSOutput, + descriptors: outputDirectory, + events: outputDirectory, + props: outputDirectory, + states: outputDirectory, + componentsAndroid: outputDirectory, + modulesAndroid: outputDirectory, + modulesCxx: outputDirectory, + tests: outputDirectory, + 'shadow-nodes': outputDirectory, + }; + + const generatedFiles: Array = []; + + for (const name of generators) { + for (const generator of LIBRARY_GENERATORS[name]) { + generator( + libraryName, + schema, + packageName, + assumeNonnull, + headerPrefix, + ).forEach((contents: string, fileName: string) => { + generatedFiles.push({ + name: fileName, + content: contents, + outputDir: outputFoldersForGenerators[name], + }); + }); + } + } + return checkOrWriteFiles(generatedFiles, test); + }, + generateFromSchemas( + {schemas, outputDirectory, supportedApplePlatforms}: SchemasOptions, + {generators, test}: SchemasConfig, + ): boolean { + Object.keys(schemas).forEach(libraryName => + schemaValidator.validate(schemas[libraryName]), + ); + + const generatedFiles: Array = []; + + for (const name of generators) { + for (const generator of SCHEMAS_GENERATORS[name]) { + generator(schemas, supportedApplePlatforms).forEach( + (contents: string, fileName: string) => { + generatedFiles.push({ + name: fileName, + content: contents, + outputDir: outputDirectory, + }); + }, + ); + } + } + return checkOrWriteFiles(generatedFiles, test); + }, + generateViewConfig({libraryName, schema}: LibraryOptions): string { + schemaValidator.validate(schema); + + const result = generateViewConfigJs + .generate(libraryName, schema) + .values() + .next(); + + if (typeof result.value !== 'string') { + throw new Error(`Failed to generate view config for ${libraryName}`); + } + + return result.value; + }, +}; diff --git a/packages/react-native-codegen/lib/generators/TypeUtils/Cxx/index.js b/packages/react-native-codegen/lib/generators/TypeUtils/Cxx/index.js new file mode 100644 index 00000000000000..59f6eaf36d3e85 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/TypeUtils/Cxx/index.js @@ -0,0 +1,17 @@ +/** + * 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 + * @oncall react_native + */ + +function wrapOptional(type, isRequired) { + return isRequired ? type : `std::optional<${type}>`; +} +module.exports = { + wrapOptional, +}; diff --git a/packages/react-native-codegen/lib/generators/TypeUtils/Cxx/index.js.flow b/packages/react-native-codegen/lib/generators/TypeUtils/Cxx/index.js.flow new file mode 100644 index 00000000000000..fcde7828a856fd --- /dev/null +++ b/packages/react-native-codegen/lib/generators/TypeUtils/Cxx/index.js.flow @@ -0,0 +1,18 @@ +/** + * 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 + * @oncall react_native + */ + +function wrapOptional(type: string, isRequired: boolean): string { + return isRequired ? type : `std::optional<${type}>`; +} + +module.exports = { + wrapOptional, +}; diff --git a/packages/react-native-codegen/lib/generators/TypeUtils/Java/index.js b/packages/react-native-codegen/lib/generators/TypeUtils/Java/index.js new file mode 100644 index 00000000000000..a87999ee7422da --- /dev/null +++ b/packages/react-native-codegen/lib/generators/TypeUtils/Java/index.js @@ -0,0 +1,32 @@ +/** + * 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 + * @oncall react_native + */ + +const objectTypeForPrimitiveType = { + boolean: 'Boolean', + double: 'Double', + float: 'Float', + int: 'Integer', +}; +function wrapOptional(type, isRequired) { + var _objectTypeForPrimiti; + return isRequired + ? type + : // $FlowFixMe[invalid-computed-prop] + `@Nullable ${ + (_objectTypeForPrimiti = objectTypeForPrimitiveType[type]) !== null && + _objectTypeForPrimiti !== void 0 + ? _objectTypeForPrimiti + : type + }`; +} +module.exports = { + wrapOptional, +}; diff --git a/packages/react-native-codegen/lib/generators/TypeUtils/Java/index.js.flow b/packages/react-native-codegen/lib/generators/TypeUtils/Java/index.js.flow new file mode 100644 index 00000000000000..b184cb6395bb22 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/TypeUtils/Java/index.js.flow @@ -0,0 +1,28 @@ +/** + * 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 + * @oncall react_native + */ + +const objectTypeForPrimitiveType = { + boolean: 'Boolean', + double: 'Double', + float: 'Float', + int: 'Integer', +}; + +function wrapOptional(type: string, isRequired: boolean): string { + return isRequired + ? type + : // $FlowFixMe[invalid-computed-prop] + `@Nullable ${objectTypeForPrimitiveType[type] ?? type}`; +} + +module.exports = { + wrapOptional, +}; diff --git a/packages/react-native-codegen/lib/generators/TypeUtils/Objective-C/index.js b/packages/react-native-codegen/lib/generators/TypeUtils/Objective-C/index.js new file mode 100644 index 00000000000000..cf394ee1801590 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/TypeUtils/Objective-C/index.js @@ -0,0 +1,17 @@ +/** + * 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 + * @oncall react_native + */ + +function wrapOptional(type, isRequired) { + return isRequired ? type : `${type} _Nullable`; +} +module.exports = { + wrapOptional, +}; diff --git a/packages/react-native-codegen/lib/generators/TypeUtils/Objective-C/index.js.flow b/packages/react-native-codegen/lib/generators/TypeUtils/Objective-C/index.js.flow new file mode 100644 index 00000000000000..529bfe64a9ad08 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/TypeUtils/Objective-C/index.js.flow @@ -0,0 +1,18 @@ +/** + * 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 + * @oncall react_native + */ + +function wrapOptional(type: string, isRequired: boolean): string { + return isRequired ? type : `${type} _Nullable`; +} + +module.exports = { + wrapOptional, +}; diff --git a/packages/react-native-codegen/lib/generators/Utils.js b/packages/react-native-codegen/lib/generators/Utils.js new file mode 100644 index 00000000000000..624e6bdb0bb4eb --- /dev/null +++ b/packages/react-native-codegen/lib/generators/Utils.js @@ -0,0 +1,47 @@ +/** + * 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'; + +function capitalize(string) { + return string.charAt(0).toUpperCase() + string.slice(1); +} +function indent(nice, spaces) { + return nice + .split('\n') + .map((line, index) => { + if (line.length === 0 || index === 0) { + return line; + } + const emptySpaces = new Array(spaces + 1).join(' '); + return emptySpaces + line; + }) + .join('\n'); +} +function toPascalCase(inString) { + if (inString.length === 0) { + return inString; + } + return inString[0].toUpperCase() + inString.slice(1); +} +function toSafeCppString(input) { + return input.split('-').map(toPascalCase).join(''); +} +function getEnumName(moduleName, origEnumName) { + const uppercasedPropName = toSafeCppString(origEnumName); + return `${moduleName}${uppercasedPropName}`; +} +module.exports = { + capitalize, + indent, + toPascalCase, + toSafeCppString, + getEnumName, +}; diff --git a/packages/react-native-codegen/lib/generators/Utils.js.flow b/packages/react-native-codegen/lib/generators/Utils.js.flow new file mode 100644 index 00000000000000..da692692daafac --- /dev/null +++ b/packages/react-native-codegen/lib/generators/Utils.js.flow @@ -0,0 +1,53 @@ +/** + * 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'; + +function capitalize(string: string): string { + return string.charAt(0).toUpperCase() + string.slice(1); +} + +function indent(nice: string, spaces: number): string { + return nice + .split('\n') + .map((line, index) => { + if (line.length === 0 || index === 0) { + return line; + } + const emptySpaces = new Array(spaces + 1).join(' '); + return emptySpaces + line; + }) + .join('\n'); +} + +function toPascalCase(inString: string): string { + if (inString.length === 0) { + return inString; + } + + return inString[0].toUpperCase() + inString.slice(1); +} + +function toSafeCppString(input: string): string { + return input.split('-').map(toPascalCase).join(''); +} + +function getEnumName(moduleName: string, origEnumName: string): string { + const uppercasedPropName = toSafeCppString(origEnumName); + return `${moduleName}${uppercasedPropName}`; +} + +module.exports = { + capitalize, + indent, + toPascalCase, + toSafeCppString, + getEnumName, +}; diff --git a/packages/react-native-codegen/lib/generators/__test_fixtures__/fixtures.js b/packages/react-native-codegen/lib/generators/__test_fixtures__/fixtures.js new file mode 100644 index 00000000000000..03d20b08e97699 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/__test_fixtures__/fixtures.js @@ -0,0 +1,81 @@ +/** + * 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 SCHEMA_WITH_TM_AND_FC = { + modules: { + ColoredView: { + type: 'Component', + components: { + ColoredView: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'color', + optional: false, + typeAnnotation: { + type: 'StringTypeAnnotation', + default: null, + }, + }, + ], + commands: [], + }, + }, + }, + NativeCalculator: { + type: 'NativeModule', + aliasMap: {}, + enumMap: {}, + spec: { + eventEmitters: [], + methods: [ + { + name: 'add', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'PromiseTypeAnnotation', + }, + params: [ + { + name: 'a', + optional: false, + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + name: 'b', + optional: false, + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + ], + }, + }, + ], + }, + moduleName: 'Calculator', + }, + }, +}; +module.exports = { + all: SCHEMA_WITH_TM_AND_FC, +}; diff --git a/packages/react-native-codegen/lib/generators/__test_fixtures__/fixtures.js.flow b/packages/react-native-codegen/lib/generators/__test_fixtures__/fixtures.js.flow new file mode 100644 index 00000000000000..d0ddfeaff730d5 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/__test_fixtures__/fixtures.js.flow @@ -0,0 +1,84 @@ +/** + * 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-local + * @format + */ + +'use strict'; + +import type {SchemaType} from '../../CodegenSchema.js'; + +const SCHEMA_WITH_TM_AND_FC: SchemaType = { + modules: { + ColoredView: { + type: 'Component', + components: { + ColoredView: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'color', + optional: false, + typeAnnotation: { + type: 'StringTypeAnnotation', + default: null, + }, + }, + ], + commands: [], + }, + }, + }, + NativeCalculator: { + type: 'NativeModule', + aliasMap: {}, + enumMap: {}, + spec: { + eventEmitters: [], + methods: [ + { + name: 'add', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'PromiseTypeAnnotation', + }, + params: [ + { + name: 'a', + optional: false, + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + name: 'b', + optional: false, + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + ], + }, + }, + ], + }, + moduleName: 'Calculator', + }, + }, +}; + +module.exports = { + all: SCHEMA_WITH_TM_AND_FC, +}; diff --git a/packages/react-native-codegen/lib/generators/components/ComponentsGeneratorUtils.js b/packages/react-native-codegen/lib/generators/components/ComponentsGeneratorUtils.js new file mode 100644 index 00000000000000..e9c8b213102be3 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/ComponentsGeneratorUtils.js @@ -0,0 +1,228 @@ +/** + * 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 _require = require('../Utils'), + getEnumName = _require.getEnumName; +const _require2 = require('./CppHelpers.js'), + generateStructName = _require2.generateStructName, + getCppTypeForAnnotation = _require2.getCppTypeForAnnotation, + getEnumMaskName = _require2.getEnumMaskName, + getImports = _require2.getImports; +function getNativeTypeFromAnnotation(componentName, prop, nameParts) { + const typeAnnotation = prop.typeAnnotation; + switch (typeAnnotation.type) { + case 'BooleanTypeAnnotation': + case 'StringTypeAnnotation': + case 'Int32TypeAnnotation': + case 'DoubleTypeAnnotation': + case 'FloatTypeAnnotation': + return getCppTypeForAnnotation(typeAnnotation.type); + case 'ReservedPropTypeAnnotation': + switch (typeAnnotation.name) { + case 'ColorPrimitive': + return 'SharedColor'; + case 'ImageSourcePrimitive': + return 'ImageSource'; + case 'ImageRequestPrimitive': + return 'ImageRequest'; + case 'PointPrimitive': + return 'Point'; + case 'EdgeInsetsPrimitive': + return 'EdgeInsets'; + case 'DimensionPrimitive': + return 'YGValue'; + default: + typeAnnotation.name; + throw new Error('Received unknown ReservedPropTypeAnnotation'); + } + case 'ArrayTypeAnnotation': { + const arrayType = typeAnnotation.elementType.type; + if (arrayType === 'ArrayTypeAnnotation') { + return `std::vector<${getNativeTypeFromAnnotation( + componentName, + { + typeAnnotation: typeAnnotation.elementType, + name: '', + }, + nameParts.concat([prop.name]), + )}>`; + } + if (arrayType === 'ObjectTypeAnnotation') { + const structName = generateStructName( + componentName, + nameParts.concat([prop.name]), + ); + return `std::vector<${structName}>`; + } + if (arrayType === 'StringEnumTypeAnnotation') { + const enumName = getEnumName(componentName, prop.name); + return getEnumMaskName(enumName); + } + const itemAnnotation = getNativeTypeFromAnnotation( + componentName, + { + typeAnnotation: typeAnnotation.elementType, + name: componentName, + }, + nameParts.concat([prop.name]), + ); + return `std::vector<${itemAnnotation}>`; + } + case 'ObjectTypeAnnotation': { + return generateStructName(componentName, nameParts.concat([prop.name])); + } + case 'StringEnumTypeAnnotation': + return getEnumName(componentName, prop.name); + case 'Int32EnumTypeAnnotation': + return getEnumName(componentName, prop.name); + case 'MixedTypeAnnotation': + return 'folly::dynamic'; + default: + typeAnnotation; + throw new Error( + `Received invalid typeAnnotation for ${componentName} prop ${prop.name}, received ${typeAnnotation.type}`, + ); + } +} + +/// This function process some types if we need to customize them +/// For example, the ImageSource and the reserved types could be trasformed into +/// const address instead of using them as plain types. +function convertTypesToConstAddressIfNeeded(type, convertibleTypes) { + if (convertibleTypes.has(type)) { + return `${type} const &`; + } + return type; +} +function convertValueToSharedPointerWithMove(type, value, convertibleTypes) { + if (convertibleTypes.has(type)) { + return `std::make_shared<${type}>(std::move(${value}))`; + } + return value; +} +function convertVariableToSharedPointer(type, convertibleTypes) { + if (convertibleTypes.has(type)) { + return `std::shared_ptr<${type}>`; + } + return type; +} +function convertVariableToPointer(type, value, convertibleTypes) { + if (convertibleTypes.has(type)) { + return `*${value}`; + } + return value; +} +const convertCtorParamToAddressType = type => { + const typesToConvert = new Set(); + typesToConvert.add('ImageSource'); + return convertTypesToConstAddressIfNeeded(type, typesToConvert); +}; +const convertCtorInitToSharedPointers = (type, value) => { + const typesToConvert = new Set(); + typesToConvert.add('ImageRequest'); + return convertValueToSharedPointerWithMove(type, value, typesToConvert); +}; +const convertGettersReturnTypeToAddressType = type => { + const typesToConvert = new Set(); + typesToConvert.add('ImageRequest'); + return convertTypesToConstAddressIfNeeded(type, typesToConvert); +}; +const convertVarTypeToSharedPointer = type => { + const typesToConvert = new Set(); + typesToConvert.add('ImageRequest'); + return convertVariableToSharedPointer(type, typesToConvert); +}; +const convertVarValueToPointer = (type, value) => { + const typesToConvert = new Set(); + typesToConvert.add('ImageRequest'); + return convertVariableToPointer(type, value, typesToConvert); +}; +function getLocalImports(properties) { + const imports = new Set(); + function addImportsForNativeName(name) { + switch (name) { + case 'ColorPrimitive': + imports.add('#include '); + return; + case 'ImageSourcePrimitive': + imports.add('#include '); + return; + case 'ImageRequestPrimitive': + imports.add('#include '); + return; + case 'PointPrimitive': + imports.add('#include '); + return; + case 'EdgeInsetsPrimitive': + imports.add('#include '); + return; + case 'DimensionPrimitive': + imports.add('#include '); + return; + default: + name; + throw new Error(`Invalid ReservedPropTypeAnnotation name, got ${name}`); + } + } + properties.forEach(prop => { + const typeAnnotation = prop.typeAnnotation; + if (typeAnnotation.type === 'ReservedPropTypeAnnotation') { + addImportsForNativeName(typeAnnotation.name); + } + if (typeAnnotation.type === 'ArrayTypeAnnotation') { + imports.add('#include '); + if (typeAnnotation.elementType.type === 'StringEnumTypeAnnotation') { + imports.add('#include '); + } + } + if ( + typeAnnotation.type === 'ArrayTypeAnnotation' && + typeAnnotation.elementType.type === 'ReservedPropTypeAnnotation' + ) { + addImportsForNativeName(typeAnnotation.elementType.name); + } + if ( + typeAnnotation.type === 'ArrayTypeAnnotation' && + typeAnnotation.elementType.type === 'ObjectTypeAnnotation' + ) { + imports.add('#include '); + const objectProps = typeAnnotation.elementType.properties; + // $FlowFixMe[incompatible-call] the type is guaranteed to be ObjectTypeAnnotation + const objectImports = getImports(objectProps); + // $FlowFixMe[incompatible-call] the type is guaranteed to be ObjectTypeAnnotation + const localImports = getLocalImports(objectProps); + // $FlowFixMe[method-unbinding] added when improving typing for this parameters + objectImports.forEach(imports.add, imports); + // $FlowFixMe[method-unbinding] added when improving typing for this parameters + localImports.forEach(imports.add, imports); + } + if (typeAnnotation.type === 'ObjectTypeAnnotation') { + imports.add('#include '); + const objectImports = getImports(typeAnnotation.properties); + const localImports = getLocalImports(typeAnnotation.properties); + // $FlowFixMe[method-unbinding] added when improving typing for this parameters + objectImports.forEach(imports.add, imports); + // $FlowFixMe[method-unbinding] added when improving typing for this parameters + localImports.forEach(imports.add, imports); + } + }); + return imports; +} +module.exports = { + getNativeTypeFromAnnotation, + convertCtorParamToAddressType, + convertGettersReturnTypeToAddressType, + convertCtorInitToSharedPointers, + convertVarTypeToSharedPointer, + convertVarValueToPointer, + getLocalImports, +}; diff --git a/packages/react-native-codegen/lib/generators/components/ComponentsGeneratorUtils.js.flow b/packages/react-native-codegen/lib/generators/components/ComponentsGeneratorUtils.js.flow new file mode 100644 index 00000000000000..3a0138224c0bf5 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/ComponentsGeneratorUtils.js.flow @@ -0,0 +1,314 @@ +/** + * 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 {NamedShape, PropTypeAnnotation} from '../../CodegenSchema'; +import type { + BooleanTypeAnnotation, + DoubleTypeAnnotation, + FloatTypeAnnotation, + Int32TypeAnnotation, + ObjectTypeAnnotation, + ReservedPropTypeAnnotation, + StringTypeAnnotation, +} from '../../CodegenSchema'; + +const {getEnumName} = require('../Utils'); +const { + generateStructName, + getCppTypeForAnnotation, + getEnumMaskName, + getImports, +} = require('./CppHelpers.js'); + +function getNativeTypeFromAnnotation( + componentName: string, + prop: + | NamedShape + | { + name: string, + typeAnnotation: + | $FlowFixMe + | DoubleTypeAnnotation + | FloatTypeAnnotation + | BooleanTypeAnnotation + | Int32TypeAnnotation + | StringTypeAnnotation + | ObjectTypeAnnotation + | ReservedPropTypeAnnotation + | { + +default: string, + +options: $ReadOnlyArray, + +type: 'StringEnumTypeAnnotation', + } + | { + +elementType: ObjectTypeAnnotation, + +type: 'ArrayTypeAnnotation', + }, + }, + nameParts: $ReadOnlyArray, +): string { + const typeAnnotation = prop.typeAnnotation; + + switch (typeAnnotation.type) { + case 'BooleanTypeAnnotation': + case 'StringTypeAnnotation': + case 'Int32TypeAnnotation': + case 'DoubleTypeAnnotation': + case 'FloatTypeAnnotation': + return getCppTypeForAnnotation(typeAnnotation.type); + case 'ReservedPropTypeAnnotation': + switch (typeAnnotation.name) { + case 'ColorPrimitive': + return 'SharedColor'; + case 'ImageSourcePrimitive': + return 'ImageSource'; + case 'ImageRequestPrimitive': + return 'ImageRequest'; + case 'PointPrimitive': + return 'Point'; + case 'EdgeInsetsPrimitive': + return 'EdgeInsets'; + case 'DimensionPrimitive': + return 'YGValue'; + default: + (typeAnnotation.name: empty); + throw new Error('Received unknown ReservedPropTypeAnnotation'); + } + case 'ArrayTypeAnnotation': { + const arrayType = typeAnnotation.elementType.type; + if (arrayType === 'ArrayTypeAnnotation') { + return `std::vector<${getNativeTypeFromAnnotation( + componentName, + {typeAnnotation: typeAnnotation.elementType, name: ''}, + nameParts.concat([prop.name]), + )}>`; + } + if (arrayType === 'ObjectTypeAnnotation') { + const structName = generateStructName( + componentName, + nameParts.concat([prop.name]), + ); + return `std::vector<${structName}>`; + } + if (arrayType === 'StringEnumTypeAnnotation') { + const enumName = getEnumName(componentName, prop.name); + return getEnumMaskName(enumName); + } + const itemAnnotation = getNativeTypeFromAnnotation( + componentName, + { + typeAnnotation: typeAnnotation.elementType, + name: componentName, + }, + nameParts.concat([prop.name]), + ); + return `std::vector<${itemAnnotation}>`; + } + case 'ObjectTypeAnnotation': { + return generateStructName(componentName, nameParts.concat([prop.name])); + } + case 'StringEnumTypeAnnotation': + return getEnumName(componentName, prop.name); + case 'Int32EnumTypeAnnotation': + return getEnumName(componentName, prop.name); + case 'MixedTypeAnnotation': + return 'folly::dynamic'; + default: + (typeAnnotation: empty); + throw new Error( + `Received invalid typeAnnotation for ${componentName} prop ${prop.name}, received ${typeAnnotation.type}`, + ); + } +} + +/// This function process some types if we need to customize them +/// For example, the ImageSource and the reserved types could be trasformed into +/// const address instead of using them as plain types. +function convertTypesToConstAddressIfNeeded( + type: string, + convertibleTypes: Set, +): string { + if (convertibleTypes.has(type)) { + return `${type} const &`; + } + return type; +} + +function convertValueToSharedPointerWithMove( + type: string, + value: string, + convertibleTypes: Set, +): string { + if (convertibleTypes.has(type)) { + return `std::make_shared<${type}>(std::move(${value}))`; + } + return value; +} + +function convertVariableToSharedPointer( + type: string, + convertibleTypes: Set, +): string { + if (convertibleTypes.has(type)) { + return `std::shared_ptr<${type}>`; + } + return type; +} + +function convertVariableToPointer( + type: string, + value: string, + convertibleTypes: Set, +): string { + if (convertibleTypes.has(type)) { + return `*${value}`; + } + return value; +} + +const convertCtorParamToAddressType = (type: string): string => { + const typesToConvert: Set = new Set(); + typesToConvert.add('ImageSource'); + + return convertTypesToConstAddressIfNeeded(type, typesToConvert); +}; + +const convertCtorInitToSharedPointers = ( + type: string, + value: string, +): string => { + const typesToConvert: Set = new Set(); + typesToConvert.add('ImageRequest'); + + return convertValueToSharedPointerWithMove(type, value, typesToConvert); +}; + +const convertGettersReturnTypeToAddressType = (type: string): string => { + const typesToConvert: Set = new Set(); + typesToConvert.add('ImageRequest'); + + return convertTypesToConstAddressIfNeeded(type, typesToConvert); +}; + +const convertVarTypeToSharedPointer = (type: string): string => { + const typesToConvert: Set = new Set(); + typesToConvert.add('ImageRequest'); + + return convertVariableToSharedPointer(type, typesToConvert); +}; + +const convertVarValueToPointer = (type: string, value: string): string => { + const typesToConvert: Set = new Set(); + typesToConvert.add('ImageRequest'); + + return convertVariableToPointer(type, value, typesToConvert); +}; + +function getLocalImports( + properties: $ReadOnlyArray>, +): Set { + const imports: Set = new Set(); + + function addImportsForNativeName( + name: + | 'ColorPrimitive' + | 'EdgeInsetsPrimitive' + | 'ImageSourcePrimitive' + | 'PointPrimitive' + | 'ImageRequestPrimitive' + | 'DimensionPrimitive', + ) { + switch (name) { + case 'ColorPrimitive': + imports.add('#include '); + return; + case 'ImageSourcePrimitive': + imports.add('#include '); + return; + case 'ImageRequestPrimitive': + imports.add('#include '); + return; + case 'PointPrimitive': + imports.add('#include '); + return; + case 'EdgeInsetsPrimitive': + imports.add('#include '); + return; + case 'DimensionPrimitive': + imports.add('#include '); + return; + default: + (name: empty); + throw new Error(`Invalid ReservedPropTypeAnnotation name, got ${name}`); + } + } + + properties.forEach(prop => { + const typeAnnotation = prop.typeAnnotation; + + if (typeAnnotation.type === 'ReservedPropTypeAnnotation') { + addImportsForNativeName(typeAnnotation.name); + } + + if (typeAnnotation.type === 'ArrayTypeAnnotation') { + imports.add('#include '); + if (typeAnnotation.elementType.type === 'StringEnumTypeAnnotation') { + imports.add('#include '); + } + } + + if ( + typeAnnotation.type === 'ArrayTypeAnnotation' && + typeAnnotation.elementType.type === 'ReservedPropTypeAnnotation' + ) { + addImportsForNativeName(typeAnnotation.elementType.name); + } + + if ( + typeAnnotation.type === 'ArrayTypeAnnotation' && + typeAnnotation.elementType.type === 'ObjectTypeAnnotation' + ) { + imports.add('#include '); + const objectProps = typeAnnotation.elementType.properties; + // $FlowFixMe[incompatible-call] the type is guaranteed to be ObjectTypeAnnotation + const objectImports = getImports(objectProps); + // $FlowFixMe[incompatible-call] the type is guaranteed to be ObjectTypeAnnotation + const localImports = getLocalImports(objectProps); + // $FlowFixMe[method-unbinding] added when improving typing for this parameters + objectImports.forEach(imports.add, imports); + // $FlowFixMe[method-unbinding] added when improving typing for this parameters + localImports.forEach(imports.add, imports); + } + + if (typeAnnotation.type === 'ObjectTypeAnnotation') { + imports.add('#include '); + const objectImports = getImports(typeAnnotation.properties); + const localImports = getLocalImports(typeAnnotation.properties); + // $FlowFixMe[method-unbinding] added when improving typing for this parameters + objectImports.forEach(imports.add, imports); + // $FlowFixMe[method-unbinding] added when improving typing for this parameters + localImports.forEach(imports.add, imports); + } + }); + + return imports; +} + +module.exports = { + getNativeTypeFromAnnotation, + convertCtorParamToAddressType, + convertGettersReturnTypeToAddressType, + convertCtorInitToSharedPointers, + convertVarTypeToSharedPointer, + convertVarValueToPointer, + getLocalImports, +}; diff --git a/packages/react-native-codegen/lib/generators/components/ComponentsProviderUtils.js b/packages/react-native-codegen/lib/generators/components/ComponentsProviderUtils.js new file mode 100644 index 00000000000000..c7d656e1815766 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/ComponentsProviderUtils.js @@ -0,0 +1,56 @@ +/** + * 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 + */ + +const APPLE_PLATFORMS_MACRO_MAP = { + ios: 'TARGET_OS_IOS', + macos: 'TARGET_OS_OSX', + tvos: 'TARGET_OS_TV', + visionos: 'TARGET_OS_VISION', +}; + +/** + * Adds compiler macros to the file template to exclude unsupported platforms. + */ +function generateSupportedApplePlatformsMacro( + fileTemplate, + supportedPlatformsMap, +) { + if (!supportedPlatformsMap) { + return fileTemplate; + } + + // According to Podspec Syntax Reference, when `platform` or `deployment_target` is not specified, it defaults to all platforms. + // https://guides.cocoapods.org/syntax/podspec.html#platform + const everyPlatformIsUnsupported = Object.keys(supportedPlatformsMap).every( + platform => supportedPlatformsMap[platform] === false, + ); + if (everyPlatformIsUnsupported) { + return fileTemplate; + } + const compilerMacroString = Object.keys(supportedPlatformsMap) + .reduce((acc, platform) => { + if (!supportedPlatformsMap[platform]) { + // $FlowFixMe[invalid-computed-prop] + return [...acc, `!${APPLE_PLATFORMS_MACRO_MAP[platform]}`]; + } + return acc; + }, []) + .join(' && '); + if (!compilerMacroString) { + return fileTemplate; + } + return `#if ${compilerMacroString} +${fileTemplate} +#endif +`; +} +module.exports = { + generateSupportedApplePlatformsMacro, +}; diff --git a/packages/react-native-codegen/lib/generators/components/ComponentsProviderUtils.js.flow b/packages/react-native-codegen/lib/generators/components/ComponentsProviderUtils.js.flow new file mode 100644 index 00000000000000..956fce3c1db8e0 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/ComponentsProviderUtils.js.flow @@ -0,0 +1,61 @@ +/** + * 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 + */ + +const APPLE_PLATFORMS_MACRO_MAP = { + ios: 'TARGET_OS_IOS', + macos: 'TARGET_OS_OSX', + tvos: 'TARGET_OS_TV', + visionos: 'TARGET_OS_VISION', +}; + +/** + * Adds compiler macros to the file template to exclude unsupported platforms. + */ +function generateSupportedApplePlatformsMacro( + fileTemplate: string, + supportedPlatformsMap: ?{[string]: boolean}, +): string { + if (!supportedPlatformsMap) { + return fileTemplate; + } + + // According to Podspec Syntax Reference, when `platform` or `deployment_target` is not specified, it defaults to all platforms. + // https://guides.cocoapods.org/syntax/podspec.html#platform + const everyPlatformIsUnsupported = Object.keys(supportedPlatformsMap).every( + platform => supportedPlatformsMap[platform] === false, + ); + + if (everyPlatformIsUnsupported) { + return fileTemplate; + } + + const compilerMacroString = Object.keys(supportedPlatformsMap) + .reduce((acc: string[], platform) => { + if (!supportedPlatformsMap[platform]) { + // $FlowFixMe[invalid-computed-prop] + return [...acc, `!${APPLE_PLATFORMS_MACRO_MAP[platform]}`]; + } + return acc; + }, []) + .join(' && '); + + if (!compilerMacroString) { + return fileTemplate; + } + + return `#if ${compilerMacroString} +${fileTemplate} +#endif +`; +} + +module.exports = { + generateSupportedApplePlatformsMacro, +}; diff --git a/packages/react-native-codegen/lib/generators/components/CppHelpers.js b/packages/react-native-codegen/lib/generators/components/CppHelpers.js new file mode 100644 index 00000000000000..21baaaee20bec4 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/CppHelpers.js @@ -0,0 +1,263 @@ +/** + * 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 _require = require('../Utils'), + getEnumName = _require.getEnumName, + toSafeCppString = _require.toSafeCppString; +function toIntEnumValueName(propName, value) { + return `${toSafeCppString(propName)}${value}`; +} +function getCppTypeForAnnotation(type) { + switch (type) { + case 'BooleanTypeAnnotation': + return 'bool'; + case 'StringTypeAnnotation': + return 'std::string'; + case 'Int32TypeAnnotation': + return 'int'; + case 'DoubleTypeAnnotation': + return 'double'; + case 'FloatTypeAnnotation': + return 'Float'; + case 'MixedTypeAnnotation': + return 'folly::dynamic'; + default: + type; + throw new Error(`Received invalid typeAnnotation ${type}`); + } +} +function getCppArrayTypeForAnnotation(typeElement, structParts) { + switch (typeElement.type) { + case 'BooleanTypeAnnotation': + case 'StringTypeAnnotation': + case 'DoubleTypeAnnotation': + case 'FloatTypeAnnotation': + case 'Int32TypeAnnotation': + case 'MixedTypeAnnotation': + return `std::vector<${getCppTypeForAnnotation(typeElement.type)}>`; + case 'StringEnumTypeAnnotation': + case 'ObjectTypeAnnotation': + if (!structParts) { + throw new Error( + `Trying to generate the event emitter for an Array of ${typeElement.type} without informations to generate the generic type`, + ); + } + return `std::vector<${generateEventStructName(structParts)}>`; + case 'ArrayTypeAnnotation': + return `std::vector<${getCppArrayTypeForAnnotation( + typeElement.elementType, + structParts, + )}>`; + default: + throw new Error( + `Can't determine array type with typeElement: ${JSON.stringify( + typeElement, + null, + 2, + )}`, + ); + } +} +function getImports(properties) { + const imports = new Set(); + function addImportsForNativeName(name) { + switch (name) { + case 'ColorPrimitive': + return; + case 'PointPrimitive': + return; + case 'EdgeInsetsPrimitive': + return; + case 'ImageRequestPrimitive': + return; + case 'ImageSourcePrimitive': + imports.add('#include '); + return; + case 'DimensionPrimitive': + imports.add('#include '); + return; + default: + name; + throw new Error(`Invalid name, got ${name}`); + } + } + properties.forEach(prop => { + const typeAnnotation = prop.typeAnnotation; + if (typeAnnotation.type === 'ReservedPropTypeAnnotation') { + addImportsForNativeName(typeAnnotation.name); + } + if ( + typeAnnotation.type === 'ArrayTypeAnnotation' && + typeAnnotation.elementType.type === 'ReservedPropTypeAnnotation' + ) { + addImportsForNativeName(typeAnnotation.elementType.name); + } + if (typeAnnotation.type === 'MixedTypeAnnotation') { + imports.add('#include '); + } + if (typeAnnotation.type === 'ObjectTypeAnnotation') { + const objectImports = getImports(typeAnnotation.properties); + // $FlowFixMe[method-unbinding] added when improving typing for this parameters + objectImports.forEach(imports.add, imports); + } + }); + return imports; +} +function generateEventStructName(parts = []) { + return parts.map(toSafeCppString).join(''); +} +function generateStructName(componentName, parts = []) { + const additional = parts.map(toSafeCppString).join(''); + return `${componentName}${additional}Struct`; +} +function getEnumMaskName(enumName) { + return `${enumName}Mask`; +} +function getDefaultInitializerString(componentName, prop) { + const defaultValue = convertDefaultTypeToString(componentName, prop); + return `{${defaultValue}}`; +} +function convertDefaultTypeToString(componentName, prop, fromBuilder = false) { + const typeAnnotation = prop.typeAnnotation; + switch (typeAnnotation.type) { + case 'BooleanTypeAnnotation': + if (typeAnnotation.default == null) { + return ''; + } + return String(typeAnnotation.default); + case 'StringTypeAnnotation': + if (typeAnnotation.default == null) { + return ''; + } + return `"${typeAnnotation.default}"`; + case 'Int32TypeAnnotation': + return String(typeAnnotation.default); + case 'DoubleTypeAnnotation': + const defaultDoubleVal = typeAnnotation.default; + return parseInt(defaultDoubleVal, 10) === defaultDoubleVal + ? typeAnnotation.default.toFixed(1) + : String(typeAnnotation.default); + case 'FloatTypeAnnotation': + const defaultFloatVal = typeAnnotation.default; + if (defaultFloatVal == null) { + return ''; + } + return parseInt(defaultFloatVal, 10) === defaultFloatVal + ? defaultFloatVal.toFixed(1) + : String(typeAnnotation.default); + case 'ReservedPropTypeAnnotation': + switch (typeAnnotation.name) { + case 'ColorPrimitive': + return ''; + case 'ImageSourcePrimitive': + return ''; + case 'ImageRequestPrimitive': + return ''; + case 'PointPrimitive': + return ''; + case 'EdgeInsetsPrimitive': + return ''; + case 'DimensionPrimitive': + return ''; + default: + typeAnnotation.name; + throw new Error( + `Unsupported type annotation: ${typeAnnotation.name}`, + ); + } + case 'ArrayTypeAnnotation': { + const elementType = typeAnnotation.elementType; + switch (elementType.type) { + case 'StringEnumTypeAnnotation': + if (elementType.default == null) { + throw new Error( + 'A default is required for array StringEnumTypeAnnotation', + ); + } + const enumName = getEnumName(componentName, prop.name); + const enumMaskName = getEnumMaskName(enumName); + const defaultValue = `${enumName}::${toSafeCppString( + elementType.default, + )}`; + if (fromBuilder) { + return `${enumMaskName}Wrapped{ .value = static_cast<${enumMaskName}>(${defaultValue})}`; + } + return `static_cast<${enumMaskName}>(${defaultValue})`; + default: + return ''; + } + } + case 'ObjectTypeAnnotation': { + return ''; + } + case 'StringEnumTypeAnnotation': + return `${getEnumName(componentName, prop.name)}::${toSafeCppString( + typeAnnotation.default, + )}`; + case 'Int32EnumTypeAnnotation': + return `${getEnumName(componentName, prop.name)}::${toIntEnumValueName( + prop.name, + typeAnnotation.default, + )}`; + case 'MixedTypeAnnotation': + return ''; + default: + typeAnnotation; + throw new Error(`Unsupported type annotation: ${typeAnnotation.type}`); + } +} +function getSourceProp(componentName, prop) { + const typeAnnotation = prop.typeAnnotation; + switch (typeAnnotation.type) { + case 'ArrayTypeAnnotation': + const elementType = typeAnnotation.elementType; + switch (elementType.type) { + case 'StringEnumTypeAnnotation': + const enumName = getEnumName(componentName, prop.name); + const enumMaskName = getEnumMaskName(enumName); + return `${enumMaskName}Wrapped{ .value = sourceProps.${prop.name} }`; + } + } + return `sourceProps.${prop.name}`; +} +function isWrappedPropType(prop) { + const typeAnnotation = prop.typeAnnotation; + switch (typeAnnotation.type) { + case 'ArrayTypeAnnotation': + const elementType = typeAnnotation.elementType; + switch (elementType.type) { + case 'StringEnumTypeAnnotation': + return true; + } + } + return false; +} +const IncludeTemplate = ({headerPrefix, file}) => { + if (headerPrefix === '') { + return `#include "${file}"`; + } + return `#include <${headerPrefix}${file}>`; +}; +module.exports = { + getDefaultInitializerString, + convertDefaultTypeToString, + getCppArrayTypeForAnnotation, + getCppTypeForAnnotation, + getEnumMaskName, + getImports, + toIntEnumValueName, + generateStructName, + generateEventStructName, + IncludeTemplate, + getSourceProp, + isWrappedPropType, +}; diff --git a/packages/react-native-codegen/lib/generators/components/CppHelpers.js.flow b/packages/react-native-codegen/lib/generators/components/CppHelpers.js.flow new file mode 100644 index 00000000000000..a208a840e9fa27 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/CppHelpers.js.flow @@ -0,0 +1,328 @@ +/** + * 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 { + EventTypeAnnotation, + NamedShape, + PropTypeAnnotation, +} from '../../CodegenSchema'; + +const {getEnumName, toSafeCppString} = require('../Utils'); + +function toIntEnumValueName(propName: string, value: number): string { + return `${toSafeCppString(propName)}${value}`; +} + +function getCppTypeForAnnotation( + type: + | 'BooleanTypeAnnotation' + | 'StringTypeAnnotation' + | 'Int32TypeAnnotation' + | 'DoubleTypeAnnotation' + | 'FloatTypeAnnotation' + | 'MixedTypeAnnotation', +): string { + switch (type) { + case 'BooleanTypeAnnotation': + return 'bool'; + case 'StringTypeAnnotation': + return 'std::string'; + case 'Int32TypeAnnotation': + return 'int'; + case 'DoubleTypeAnnotation': + return 'double'; + case 'FloatTypeAnnotation': + return 'Float'; + case 'MixedTypeAnnotation': + return 'folly::dynamic'; + default: + (type: empty); + throw new Error(`Received invalid typeAnnotation ${type}`); + } +} + +function getCppArrayTypeForAnnotation( + typeElement: EventTypeAnnotation, + structParts?: string[], +): string { + switch (typeElement.type) { + case 'BooleanTypeAnnotation': + case 'StringTypeAnnotation': + case 'DoubleTypeAnnotation': + case 'FloatTypeAnnotation': + case 'Int32TypeAnnotation': + case 'MixedTypeAnnotation': + return `std::vector<${getCppTypeForAnnotation(typeElement.type)}>`; + case 'StringEnumTypeAnnotation': + case 'ObjectTypeAnnotation': + if (!structParts) { + throw new Error( + `Trying to generate the event emitter for an Array of ${typeElement.type} without informations to generate the generic type`, + ); + } + return `std::vector<${generateEventStructName(structParts)}>`; + case 'ArrayTypeAnnotation': + return `std::vector<${getCppArrayTypeForAnnotation( + typeElement.elementType, + structParts, + )}>`; + default: + throw new Error( + `Can't determine array type with typeElement: ${JSON.stringify( + typeElement, + null, + 2, + )}`, + ); + } +} + +function getImports( + properties: + | $ReadOnlyArray> + | $ReadOnlyArray>, +): Set { + const imports: Set = new Set(); + + function addImportsForNativeName( + name: + | 'ColorPrimitive' + | 'EdgeInsetsPrimitive' + | 'ImageRequestPrimitive' + | 'ImageSourcePrimitive' + | 'PointPrimitive' + | 'DimensionPrimitive', + ) { + switch (name) { + case 'ColorPrimitive': + return; + case 'PointPrimitive': + return; + case 'EdgeInsetsPrimitive': + return; + case 'ImageRequestPrimitive': + return; + case 'ImageSourcePrimitive': + imports.add('#include '); + return; + case 'DimensionPrimitive': + imports.add('#include '); + return; + default: + (name: empty); + throw new Error(`Invalid name, got ${name}`); + } + } + + properties.forEach(prop => { + const typeAnnotation = prop.typeAnnotation; + + if (typeAnnotation.type === 'ReservedPropTypeAnnotation') { + addImportsForNativeName(typeAnnotation.name); + } + + if ( + typeAnnotation.type === 'ArrayTypeAnnotation' && + typeAnnotation.elementType.type === 'ReservedPropTypeAnnotation' + ) { + addImportsForNativeName(typeAnnotation.elementType.name); + } + + if (typeAnnotation.type === 'MixedTypeAnnotation') { + imports.add('#include '); + } + + if (typeAnnotation.type === 'ObjectTypeAnnotation') { + const objectImports = getImports(typeAnnotation.properties); + // $FlowFixMe[method-unbinding] added when improving typing for this parameters + objectImports.forEach(imports.add, imports); + } + }); + + return imports; +} + +function generateEventStructName(parts: $ReadOnlyArray = []): string { + return parts.map(toSafeCppString).join(''); +} + +function generateStructName( + componentName: string, + parts: $ReadOnlyArray = [], +): string { + const additional = parts.map(toSafeCppString).join(''); + return `${componentName}${additional}Struct`; +} + +function getEnumMaskName(enumName: string): string { + return `${enumName}Mask`; +} + +function getDefaultInitializerString( + componentName: string, + prop: NamedShape, +): string { + const defaultValue = convertDefaultTypeToString(componentName, prop); + return `{${defaultValue}}`; +} + +function convertDefaultTypeToString( + componentName: string, + prop: NamedShape, + fromBuilder: boolean = false, +): string { + const typeAnnotation = prop.typeAnnotation; + switch (typeAnnotation.type) { + case 'BooleanTypeAnnotation': + if (typeAnnotation.default == null) { + return ''; + } + return String(typeAnnotation.default); + case 'StringTypeAnnotation': + if (typeAnnotation.default == null) { + return ''; + } + return `"${typeAnnotation.default}"`; + case 'Int32TypeAnnotation': + return String(typeAnnotation.default); + case 'DoubleTypeAnnotation': + const defaultDoubleVal = typeAnnotation.default; + return parseInt(defaultDoubleVal, 10) === defaultDoubleVal + ? typeAnnotation.default.toFixed(1) + : String(typeAnnotation.default); + case 'FloatTypeAnnotation': + const defaultFloatVal = typeAnnotation.default; + if (defaultFloatVal == null) { + return ''; + } + return parseInt(defaultFloatVal, 10) === defaultFloatVal + ? defaultFloatVal.toFixed(1) + : String(typeAnnotation.default); + case 'ReservedPropTypeAnnotation': + switch (typeAnnotation.name) { + case 'ColorPrimitive': + return ''; + case 'ImageSourcePrimitive': + return ''; + case 'ImageRequestPrimitive': + return ''; + case 'PointPrimitive': + return ''; + case 'EdgeInsetsPrimitive': + return ''; + case 'DimensionPrimitive': + return ''; + default: + (typeAnnotation.name: empty); + throw new Error( + `Unsupported type annotation: ${typeAnnotation.name}`, + ); + } + case 'ArrayTypeAnnotation': { + const elementType = typeAnnotation.elementType; + switch (elementType.type) { + case 'StringEnumTypeAnnotation': + if (elementType.default == null) { + throw new Error( + 'A default is required for array StringEnumTypeAnnotation', + ); + } + const enumName = getEnumName(componentName, prop.name); + const enumMaskName = getEnumMaskName(enumName); + const defaultValue = `${enumName}::${toSafeCppString( + elementType.default, + )}`; + if (fromBuilder) { + return `${enumMaskName}Wrapped{ .value = static_cast<${enumMaskName}>(${defaultValue})}`; + } + return `static_cast<${enumMaskName}>(${defaultValue})`; + default: + return ''; + } + } + case 'ObjectTypeAnnotation': { + return ''; + } + case 'StringEnumTypeAnnotation': + return `${getEnumName(componentName, prop.name)}::${toSafeCppString( + typeAnnotation.default, + )}`; + case 'Int32EnumTypeAnnotation': + return `${getEnumName(componentName, prop.name)}::${toIntEnumValueName( + prop.name, + typeAnnotation.default, + )}`; + case 'MixedTypeAnnotation': + return ''; + default: + (typeAnnotation: empty); + throw new Error(`Unsupported type annotation: ${typeAnnotation.type}`); + } +} + +function getSourceProp( + componentName: string, + prop: NamedShape, +): string { + const typeAnnotation = prop.typeAnnotation; + switch (typeAnnotation.type) { + case 'ArrayTypeAnnotation': + const elementType = typeAnnotation.elementType; + switch (elementType.type) { + case 'StringEnumTypeAnnotation': + const enumName = getEnumName(componentName, prop.name); + const enumMaskName = getEnumMaskName(enumName); + return `${enumMaskName}Wrapped{ .value = sourceProps.${prop.name} }`; + } + } + return `sourceProps.${prop.name}`; +} + +function isWrappedPropType(prop: NamedShape): boolean { + const typeAnnotation = prop.typeAnnotation; + switch (typeAnnotation.type) { + case 'ArrayTypeAnnotation': + const elementType = typeAnnotation.elementType; + switch (elementType.type) { + case 'StringEnumTypeAnnotation': + return true; + } + } + return false; +} + +const IncludeTemplate = ({ + headerPrefix, + file, +}: { + headerPrefix: string, + file: string, +}): string => { + if (headerPrefix === '') { + return `#include "${file}"`; + } + return `#include <${headerPrefix}${file}>`; +}; + +module.exports = { + getDefaultInitializerString, + convertDefaultTypeToString, + getCppArrayTypeForAnnotation, + getCppTypeForAnnotation, + getEnumMaskName, + getImports, + toIntEnumValueName, + generateStructName, + generateEventStructName, + IncludeTemplate, + getSourceProp, + isWrappedPropType, +}; diff --git a/packages/react-native-codegen/lib/generators/components/GenerateComponentDescriptorCpp.js b/packages/react-native-codegen/lib/generators/components/GenerateComponentDescriptorCpp.js new file mode 100644 index 00000000000000..e39651a40555e9 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/GenerateComponentDescriptorCpp.js @@ -0,0 +1,89 @@ +/** + * 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 _require = require('./CppHelpers'), + IncludeTemplate = _require.IncludeTemplate; + +// File path -> contents + +const FileTemplate = ({libraryName, componentRegistrations, headerPrefix}) => ` +/** + * 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: GenerateComponentDescriptorCpp.js + */ + +${IncludeTemplate({ + headerPrefix, + file: 'ComponentDescriptors.h', +})} +#include +#include + +namespace facebook::react { + +void ${libraryName}_registerComponentDescriptorsFromCodegen( + std::shared_ptr registry) { +${componentRegistrations} +} + +} // namespace facebook::react +`; +const ComponentRegistrationTemplate = ({className}) => + ` +registry->add(concreteComponentDescriptorProvider<${className}ComponentDescriptor>()); +`.trim(); +module.exports = { + generate( + libraryName, + schema, + packageName, + assumeNonnull = false, + headerPrefix, + ) { + const fileName = 'ComponentDescriptors.cpp'; + const componentRegistrations = Object.keys(schema.modules) + .map(moduleName => { + const module = schema.modules[moduleName]; + if (module.type !== 'Component') { + return; + } + const components = module.components; + // No components in this module + if (components == null) { + return null; + } + return Object.keys(components) + .map(componentName => { + if (components[componentName].interfaceOnly === true) { + return; + } + return ComponentRegistrationTemplate({ + className: componentName, + }); + }) + .join('\n'); + }) + .filter(Boolean) + .join('\n'); + const replacedTemplate = FileTemplate({ + libraryName, + componentRegistrations, + headerPrefix: + headerPrefix !== null && headerPrefix !== void 0 ? headerPrefix : '', + }); + return new Map([[fileName, replacedTemplate]]); + }, +}; diff --git a/packages/react-native-codegen/lib/generators/components/GenerateComponentDescriptorCpp.js.flow b/packages/react-native-codegen/lib/generators/components/GenerateComponentDescriptorCpp.js.flow new file mode 100644 index 00000000000000..794e04765742a2 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/GenerateComponentDescriptorCpp.js.flow @@ -0,0 +1,101 @@ +/** + * 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 {SchemaType} from '../../CodegenSchema'; + +const {IncludeTemplate} = require('./CppHelpers'); + +// File path -> contents +type FilesOutput = Map; + +const FileTemplate = ({ + libraryName, + componentRegistrations, + headerPrefix, +}: { + libraryName: string, + componentRegistrations: string, + headerPrefix: 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: GenerateComponentDescriptorCpp.js + */ + +${IncludeTemplate({headerPrefix, file: 'ComponentDescriptors.h'})} +#include +#include + +namespace facebook::react { + +void ${libraryName}_registerComponentDescriptorsFromCodegen( + std::shared_ptr registry) { +${componentRegistrations} +} + +} // namespace facebook::react +`; + +const ComponentRegistrationTemplate = ({className}: {className: string}) => + ` +registry->add(concreteComponentDescriptorProvider<${className}ComponentDescriptor>()); +`.trim(); + +module.exports = { + generate( + libraryName: string, + schema: SchemaType, + packageName?: string, + assumeNonnull: boolean = false, + headerPrefix?: string, + ): FilesOutput { + const fileName = 'ComponentDescriptors.cpp'; + + const componentRegistrations = 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 => { + if (components[componentName].interfaceOnly === true) { + return; + } + + return ComponentRegistrationTemplate({className: componentName}); + }) + .join('\n'); + }) + .filter(Boolean) + .join('\n'); + + const replacedTemplate = FileTemplate({ + libraryName, + componentRegistrations, + headerPrefix: headerPrefix ?? '', + }); + + return new Map([[fileName, replacedTemplate]]); + }, +}; diff --git a/packages/react-native-codegen/lib/generators/components/GenerateComponentDescriptorH.js b/packages/react-native-codegen/lib/generators/components/GenerateComponentDescriptorH.js new file mode 100644 index 00000000000000..5c0d78c06d2c52 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/GenerateComponentDescriptorH.js @@ -0,0 +1,91 @@ +/** + * 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 _require = require('./CppHelpers'), + IncludeTemplate = _require.IncludeTemplate; + +// File path -> contents + +const FileTemplate = ({libraryName, componentDefinitions, headerPrefix}) => ` +/** + * 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: GenerateComponentDescriptorH.js + */ + +#pragma once + +${IncludeTemplate({ + headerPrefix, + file: 'ShadowNodes.h', +})} +#include +#include + +namespace facebook::react { + +${componentDefinitions} + +void ${libraryName}_registerComponentDescriptorsFromCodegen( + std::shared_ptr registry); + +} // namespace facebook::react +`; +const ComponentDefinitionTemplate = ({className}) => + ` +using ${className}ComponentDescriptor = ConcreteComponentDescriptor<${className}ShadowNode>; +`.trim(); +module.exports = { + generate( + libraryName, + schema, + packageName, + assumeNonnull = false, + headerPrefix, + ) { + const fileName = 'ComponentDescriptors.h'; + const componentDefinitions = Object.keys(schema.modules) + .map(moduleName => { + const module = schema.modules[moduleName]; + if (module.type !== 'Component') { + return; + } + const components = module.components; + // No components in this module + if (components == null) { + return null; + } + return Object.keys(components) + .map(componentName => { + if (components[componentName].interfaceOnly === true) { + return; + } + return ComponentDefinitionTemplate({ + className: componentName, + }); + }) + .join('\n'); + }) + .filter(Boolean) + .join('\n'); + const replacedTemplate = FileTemplate({ + libraryName, + componentDefinitions, + headerPrefix: + headerPrefix !== null && headerPrefix !== void 0 ? headerPrefix : '', + }); + return new Map([[fileName, replacedTemplate]]); + }, +}; diff --git a/packages/react-native-codegen/lib/generators/components/GenerateComponentDescriptorH.js.flow b/packages/react-native-codegen/lib/generators/components/GenerateComponentDescriptorH.js.flow new file mode 100644 index 00000000000000..0a68446cd1af71 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/GenerateComponentDescriptorH.js.flow @@ -0,0 +1,103 @@ +/** + * 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 {SchemaType} from '../../CodegenSchema'; + +const {IncludeTemplate} = require('./CppHelpers'); + +// File path -> contents +type FilesOutput = Map; + +const FileTemplate = ({ + libraryName, + componentDefinitions, + headerPrefix, +}: { + libraryName: string, + componentDefinitions: string, + headerPrefix: 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: GenerateComponentDescriptorH.js + */ + +#pragma once + +${IncludeTemplate({headerPrefix, file: 'ShadowNodes.h'})} +#include +#include + +namespace facebook::react { + +${componentDefinitions} + +void ${libraryName}_registerComponentDescriptorsFromCodegen( + std::shared_ptr registry); + +} // namespace facebook::react +`; + +const ComponentDefinitionTemplate = ({className}: {className: string}) => + ` +using ${className}ComponentDescriptor = ConcreteComponentDescriptor<${className}ShadowNode>; +`.trim(); + +module.exports = { + generate( + libraryName: string, + schema: SchemaType, + packageName?: string, + assumeNonnull: boolean = false, + headerPrefix?: string, + ): FilesOutput { + const fileName = 'ComponentDescriptors.h'; + + const componentDefinitions = 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 => { + if (components[componentName].interfaceOnly === true) { + return; + } + + return ComponentDefinitionTemplate({className: componentName}); + }) + .join('\n'); + }) + .filter(Boolean) + .join('\n'); + + const replacedTemplate = FileTemplate({ + libraryName, + componentDefinitions, + headerPrefix: headerPrefix ?? '', + }); + + return new Map([[fileName, replacedTemplate]]); + }, +}; diff --git a/packages/react-native-codegen/lib/generators/components/GenerateComponentHObjCpp.js b/packages/react-native-codegen/lib/generators/components/GenerateComponentHObjCpp.js new file mode 100644 index 00000000000000..ebbe01feb5d25b --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/GenerateComponentHObjCpp.js @@ -0,0 +1,341 @@ +/** + * 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'; + +function getOrdinalNumber(num) { + switch (num) { + case 1: + return '1st'; + case 2: + return '2nd'; + case 3: + return '3rd'; + } + if (num <= 20) { + return `${num}th`; + } + return 'unknown'; +} +const ProtocolTemplate = ({componentName, methods}) => + ` +@protocol RCT${componentName}ViewProtocol +${methods} +@end +`.trim(); +const CommandHandlerIfCaseConvertArgTemplate = ({ + componentName, + expectedKind, + argNumber, + argNumberString, + expectedKindString, + argConversion, +}) => + ` + NSObject *arg${argNumber} = args[${argNumber}]; +#if RCT_DEBUG + if (!RCTValidateTypeOfViewCommandArgument(arg${argNumber}, ${expectedKind}, @"${expectedKindString}", @"${componentName}", commandName, @"${argNumberString}")) { + return; + } +#endif + ${argConversion} +`.trim(); +const CommandHandlerIfCaseTemplate = ({ + componentName, + commandName, + numArgs, + convertArgs, + commandCall, +}) => + ` +if ([commandName isEqualToString:@"${commandName}"]) { +#if RCT_DEBUG + if ([args count] != ${numArgs}) { + RCTLogError(@"%@ command %@ received %d arguments, expected %d.", @"${componentName}", commandName, (int)[args count], ${numArgs}); + return; + } +#endif + + ${convertArgs} + + ${commandCall} + return; +} +`.trim(); +const CommandHandlerTemplate = ({componentName, ifCases}) => + ` +RCT_EXTERN inline void RCT${componentName}HandleCommand( + id componentView, + NSString const *commandName, + NSArray const *args) +{ + ${ifCases} + +#if RCT_DEBUG + RCTLogError(@"%@ received command %@, which is not a supported command.", @"${componentName}", commandName); +#endif +} +`.trim(); +const FileTemplate = ({componentContent}) => + ` +/** +* 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: GenerateComponentHObjCpp.js +*/ + +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +${componentContent} + +NS_ASSUME_NONNULL_END +`.trim(); +function getObjCParamType(param) { + const typeAnnotation = param.typeAnnotation; + switch (typeAnnotation.type) { + case 'ReservedTypeAnnotation': + switch (typeAnnotation.name) { + case 'RootTag': + return 'double'; + default: + typeAnnotation.name; + throw new Error(`Receieved invalid type: ${typeAnnotation.name}`); + } + case 'BooleanTypeAnnotation': + return 'BOOL'; + case 'DoubleTypeAnnotation': + return 'double'; + case 'FloatTypeAnnotation': + return 'float'; + case 'Int32TypeAnnotation': + return 'NSInteger'; + case 'StringTypeAnnotation': + return 'NSString *'; + case 'ArrayTypeAnnotation': + return 'const NSArray *'; + default: + typeAnnotation.type; + throw new Error('Received invalid param type annotation'); + } +} +function getObjCExpectedKindParamType(param) { + const typeAnnotation = param.typeAnnotation; + switch (typeAnnotation.type) { + case 'ReservedTypeAnnotation': + switch (typeAnnotation.name) { + case 'RootTag': + return '[NSNumber class]'; + default: + typeAnnotation.name; + throw new Error(`Receieved invalid type: ${typeAnnotation.name}`); + } + case 'BooleanTypeAnnotation': + return '[NSNumber class]'; + case 'DoubleTypeAnnotation': + return '[NSNumber class]'; + case 'FloatTypeAnnotation': + return '[NSNumber class]'; + case 'Int32TypeAnnotation': + return '[NSNumber class]'; + case 'StringTypeAnnotation': + return '[NSString class]'; + case 'ArrayTypeAnnotation': + return '[NSArray class]'; + default: + typeAnnotation.type; + throw new Error('Received invalid param type annotation'); + } +} +function getReadableExpectedKindParamType(param) { + const typeAnnotation = param.typeAnnotation; + switch (typeAnnotation.type) { + case 'ReservedTypeAnnotation': + switch (typeAnnotation.name) { + case 'RootTag': + return 'double'; + default: + typeAnnotation.name; + throw new Error(`Receieved invalid type: ${typeAnnotation.name}`); + } + case 'BooleanTypeAnnotation': + return 'boolean'; + case 'DoubleTypeAnnotation': + return 'double'; + case 'FloatTypeAnnotation': + return 'float'; + case 'Int32TypeAnnotation': + return 'number'; + case 'StringTypeAnnotation': + return 'string'; + case 'ArrayTypeAnnotation': + return 'array'; + default: + typeAnnotation.type; + throw new Error('Received invalid param type annotation'); + } +} +function getObjCRightHandAssignmentParamType(param, index) { + const typeAnnotation = param.typeAnnotation; + switch (typeAnnotation.type) { + case 'ReservedTypeAnnotation': + switch (typeAnnotation.name) { + case 'RootTag': + return `[(NSNumber *)arg${index} doubleValue]`; + default: + typeAnnotation.name; + throw new Error(`Receieved invalid type: ${typeAnnotation.name}`); + } + case 'BooleanTypeAnnotation': + return `[(NSNumber *)arg${index} boolValue]`; + case 'DoubleTypeAnnotation': + return `[(NSNumber *)arg${index} doubleValue]`; + case 'FloatTypeAnnotation': + return `[(NSNumber *)arg${index} floatValue]`; + case 'Int32TypeAnnotation': + return `[(NSNumber *)arg${index} intValue]`; + case 'StringTypeAnnotation': + return `(NSString *)arg${index}`; + case 'ArrayTypeAnnotation': + return `(NSArray *)arg${index}`; + default: + typeAnnotation.type; + throw new Error('Received invalid param type annotation'); + } +} +function generateProtocol(component, componentName) { + const methods = component.commands + .map(command => { + const params = command.typeAnnotation.params; + const paramString = + params.length === 0 + ? '' + : params + .map((param, index) => { + const objCType = getObjCParamType(param); + return `${index === 0 ? '' : param.name}:(${objCType})${ + param.name + }`; + }) + .join(' '); + return `- (void)${command.name}${paramString};`; + }) + .join('\n') + .trim(); + return ProtocolTemplate({ + componentName, + methods, + }); +} +function generateConvertAndValidateParam(param, index, componentName) { + const leftSideType = getObjCParamType(param); + const expectedKind = getObjCExpectedKindParamType(param); + const expectedKindString = getReadableExpectedKindParamType(param); + const argConversion = `${leftSideType} ${ + param.name + } = ${getObjCRightHandAssignmentParamType(param, index)};`; + return CommandHandlerIfCaseConvertArgTemplate({ + componentName, + argConversion, + argNumber: index, + argNumberString: getOrdinalNumber(index + 1), + expectedKind, + expectedKindString, + }); +} +function generateCommandIfCase(command, componentName) { + const params = command.typeAnnotation.params; + const convertArgs = params + .map((param, index) => + generateConvertAndValidateParam(param, index, componentName), + ) + .join('\n\n') + .trim(); + const commandCallArgs = + params.length === 0 + ? '' + : params + .map((param, index) => { + return `${index === 0 ? '' : param.name}:${param.name}`; + }) + .join(' '); + const commandCall = `[componentView ${command.name}${commandCallArgs}];`; + return CommandHandlerIfCaseTemplate({ + componentName, + commandName: command.name, + numArgs: params.length, + convertArgs, + commandCall, + }); +} +function generateCommandHandler(component, componentName) { + if (component.commands.length === 0) { + return null; + } + const ifCases = component.commands + .map(command => generateCommandIfCase(command, componentName)) + .join('\n\n'); + return CommandHandlerTemplate({ + componentName, + ifCases, + }); +} +module.exports = { + generate( + libraryName, + schema, + packageName, + assumeNonnull = false, + headerPrefix, + ) { + const fileName = 'RCTComponentViewHelpers.h'; + const componentContent = Object.keys(schema.modules) + .map(moduleName => { + const module = schema.modules[moduleName]; + if (module.type !== 'Component') { + return; + } + const components = module.components; + // No components in this module + if (components == null) { + return null; + } + return Object.keys(components) + .filter(componentName => { + const component = components[componentName]; + return !( + component.excludedPlatforms && + component.excludedPlatforms.includes('iOS') + ); + }) + .map(componentName => { + return [ + generateProtocol(components[componentName], componentName), + generateCommandHandler(components[componentName], componentName), + ] + .join('\n\n') + .trim(); + }) + .join('\n\n'); + }) + .filter(Boolean) + .join('\n\n'); + const replacedTemplate = FileTemplate({ + componentContent, + }); + return new Map([[fileName, replacedTemplate]]); + }, +}; diff --git a/packages/react-native-codegen/lib/generators/components/GenerateComponentHObjCpp.js.flow b/packages/react-native-codegen/lib/generators/components/GenerateComponentHObjCpp.js.flow new file mode 100644 index 00000000000000..79724d1810eb67 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/GenerateComponentHObjCpp.js.flow @@ -0,0 +1,427 @@ +/** + * 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 { + CommandParamTypeAnnotation, + CommandTypeAnnotation, + ComponentShape, + NamedShape, + SchemaType, +} from '../../CodegenSchema'; + +type FilesOutput = Map; + +function getOrdinalNumber(num: number): string { + switch (num) { + case 1: + return '1st'; + case 2: + return '2nd'; + case 3: + return '3rd'; + } + + if (num <= 20) { + return `${num}th`; + } + + return 'unknown'; +} + +const ProtocolTemplate = ({ + componentName, + methods, +}: { + componentName: string, + methods: string, +}) => + ` +@protocol RCT${componentName}ViewProtocol +${methods} +@end +`.trim(); + +const CommandHandlerIfCaseConvertArgTemplate = ({ + componentName, + expectedKind, + argNumber, + argNumberString, + expectedKindString, + argConversion, +}: { + componentName: string, + expectedKind: string, + argNumber: number, + argNumberString: string, + expectedKindString: string, + argConversion: string, +}) => + ` + NSObject *arg${argNumber} = args[${argNumber}]; +#if RCT_DEBUG + if (!RCTValidateTypeOfViewCommandArgument(arg${argNumber}, ${expectedKind}, @"${expectedKindString}", @"${componentName}", commandName, @"${argNumberString}")) { + return; + } +#endif + ${argConversion} +`.trim(); + +const CommandHandlerIfCaseTemplate = ({ + componentName, + commandName, + numArgs, + convertArgs, + commandCall, +}: { + componentName: string, + commandName: string, + numArgs: number, + convertArgs: string, + commandCall: string, +}) => + ` +if ([commandName isEqualToString:@"${commandName}"]) { +#if RCT_DEBUG + if ([args count] != ${numArgs}) { + RCTLogError(@"%@ command %@ received %d arguments, expected %d.", @"${componentName}", commandName, (int)[args count], ${numArgs}); + return; + } +#endif + + ${convertArgs} + + ${commandCall} + return; +} +`.trim(); + +const CommandHandlerTemplate = ({ + componentName, + ifCases, +}: { + componentName: string, + ifCases: string, +}) => + ` +RCT_EXTERN inline void RCT${componentName}HandleCommand( + id componentView, + NSString const *commandName, + NSArray const *args) +{ + ${ifCases} + +#if RCT_DEBUG + RCTLogError(@"%@ received command %@, which is not a supported command.", @"${componentName}", commandName); +#endif +} +`.trim(); + +const FileTemplate = ({componentContent}: {componentContent: 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: GenerateComponentHObjCpp.js +*/ + +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +${componentContent} + +NS_ASSUME_NONNULL_END +`.trim(); + +type Param = NamedShape; + +function getObjCParamType(param: Param): string { + const {typeAnnotation} = param; + + switch (typeAnnotation.type) { + case 'ReservedTypeAnnotation': + switch (typeAnnotation.name) { + case 'RootTag': + return 'double'; + default: + (typeAnnotation.name: empty); + throw new Error(`Receieved invalid type: ${typeAnnotation.name}`); + } + case 'BooleanTypeAnnotation': + return 'BOOL'; + case 'DoubleTypeAnnotation': + return 'double'; + case 'FloatTypeAnnotation': + return 'float'; + case 'Int32TypeAnnotation': + return 'NSInteger'; + case 'StringTypeAnnotation': + return 'NSString *'; + case 'ArrayTypeAnnotation': + return 'const NSArray *'; + default: + (typeAnnotation.type: empty); + throw new Error('Received invalid param type annotation'); + } +} + +function getObjCExpectedKindParamType(param: Param): string { + const {typeAnnotation} = param; + + switch (typeAnnotation.type) { + case 'ReservedTypeAnnotation': + switch (typeAnnotation.name) { + case 'RootTag': + return '[NSNumber class]'; + default: + (typeAnnotation.name: empty); + throw new Error(`Receieved invalid type: ${typeAnnotation.name}`); + } + case 'BooleanTypeAnnotation': + return '[NSNumber class]'; + case 'DoubleTypeAnnotation': + return '[NSNumber class]'; + case 'FloatTypeAnnotation': + return '[NSNumber class]'; + case 'Int32TypeAnnotation': + return '[NSNumber class]'; + case 'StringTypeAnnotation': + return '[NSString class]'; + case 'ArrayTypeAnnotation': + return '[NSArray class]'; + default: + (typeAnnotation.type: empty); + throw new Error('Received invalid param type annotation'); + } +} + +function getReadableExpectedKindParamType(param: Param): string { + const {typeAnnotation} = param; + + switch (typeAnnotation.type) { + case 'ReservedTypeAnnotation': + switch (typeAnnotation.name) { + case 'RootTag': + return 'double'; + default: + (typeAnnotation.name: empty); + throw new Error(`Receieved invalid type: ${typeAnnotation.name}`); + } + case 'BooleanTypeAnnotation': + return 'boolean'; + case 'DoubleTypeAnnotation': + return 'double'; + case 'FloatTypeAnnotation': + return 'float'; + case 'Int32TypeAnnotation': + return 'number'; + case 'StringTypeAnnotation': + return 'string'; + case 'ArrayTypeAnnotation': + return 'array'; + default: + (typeAnnotation.type: empty); + throw new Error('Received invalid param type annotation'); + } +} + +function getObjCRightHandAssignmentParamType( + param: Param, + index: number, +): string { + const {typeAnnotation} = param; + + switch (typeAnnotation.type) { + case 'ReservedTypeAnnotation': + switch (typeAnnotation.name) { + case 'RootTag': + return `[(NSNumber *)arg${index} doubleValue]`; + default: + (typeAnnotation.name: empty); + throw new Error(`Receieved invalid type: ${typeAnnotation.name}`); + } + case 'BooleanTypeAnnotation': + return `[(NSNumber *)arg${index} boolValue]`; + case 'DoubleTypeAnnotation': + return `[(NSNumber *)arg${index} doubleValue]`; + case 'FloatTypeAnnotation': + return `[(NSNumber *)arg${index} floatValue]`; + case 'Int32TypeAnnotation': + return `[(NSNumber *)arg${index} intValue]`; + case 'StringTypeAnnotation': + return `(NSString *)arg${index}`; + case 'ArrayTypeAnnotation': + return `(NSArray *)arg${index}`; + default: + (typeAnnotation.type: empty); + throw new Error('Received invalid param type annotation'); + } +} + +function generateProtocol( + component: ComponentShape, + componentName: string, +): string { + const methods = component.commands + .map(command => { + const params = command.typeAnnotation.params; + const paramString = + params.length === 0 + ? '' + : params + .map((param, index) => { + const objCType = getObjCParamType(param); + + return `${index === 0 ? '' : param.name}:(${objCType})${ + param.name + }`; + }) + .join(' '); + return `- (void)${command.name}${paramString};`; + }) + .join('\n') + .trim(); + + return ProtocolTemplate({ + componentName, + methods, + }); +} + +function generateConvertAndValidateParam( + param: Param, + index: number, + componentName: string, +): string { + const leftSideType = getObjCParamType(param); + const expectedKind = getObjCExpectedKindParamType(param); + const expectedKindString = getReadableExpectedKindParamType(param); + const argConversion = `${leftSideType} ${ + param.name + } = ${getObjCRightHandAssignmentParamType(param, index)};`; + + return CommandHandlerIfCaseConvertArgTemplate({ + componentName, + argConversion, + argNumber: index, + argNumberString: getOrdinalNumber(index + 1), + expectedKind, + expectedKindString, + }); +} + +function generateCommandIfCase( + command: NamedShape, + componentName: string, +) { + const params = command.typeAnnotation.params; + + const convertArgs = params + .map((param, index) => + generateConvertAndValidateParam(param, index, componentName), + ) + .join('\n\n') + .trim(); + + const commandCallArgs = + params.length === 0 + ? '' + : params + .map((param, index) => { + return `${index === 0 ? '' : param.name}:${param.name}`; + }) + .join(' '); + const commandCall = `[componentView ${command.name}${commandCallArgs}];`; + + return CommandHandlerIfCaseTemplate({ + componentName, + commandName: command.name, + numArgs: params.length, + convertArgs, + commandCall, + }); +} + +function generateCommandHandler( + component: ComponentShape, + componentName: string, +): ?string { + if (component.commands.length === 0) { + return null; + } + + const ifCases = component.commands + .map(command => generateCommandIfCase(command, componentName)) + .join('\n\n'); + + return CommandHandlerTemplate({ + componentName, + ifCases, + }); +} + +module.exports = { + generate( + libraryName: string, + schema: SchemaType, + packageName?: string, + assumeNonnull: boolean = false, + headerPrefix?: string, + ): FilesOutput { + const fileName = 'RCTComponentViewHelpers.h'; + + const componentContent = 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) + .filter(componentName => { + const component = components[componentName]; + return !( + component.excludedPlatforms && + component.excludedPlatforms.includes('iOS') + ); + }) + .map(componentName => { + return [ + generateProtocol(components[componentName], componentName), + generateCommandHandler(components[componentName], componentName), + ] + .join('\n\n') + .trim(); + }) + .join('\n\n'); + }) + .filter(Boolean) + .join('\n\n'); + + const replacedTemplate = FileTemplate({ + componentContent, + }); + + return new Map([[fileName, replacedTemplate]]); + }, +}; diff --git a/packages/react-native-codegen/lib/generators/components/GenerateEventEmitterCpp.js b/packages/react-native-codegen/lib/generators/components/GenerateEventEmitterCpp.js new file mode 100644 index 00000000000000..3cf4edbcf4f0d3 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/GenerateEventEmitterCpp.js @@ -0,0 +1,399 @@ +/** + * 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 _require = require('../Utils'), + indent = _require.indent; +const _require2 = require('./CppHelpers'), + IncludeTemplate = _require2.IncludeTemplate, + generateEventStructName = _require2.generateEventStructName; + +// File path -> contents + +const FileTemplate = ({events, extraIncludes, headerPrefix}) => ` +/** + * 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: GenerateEventEmitterCpp.js + */ + +${IncludeTemplate({ + headerPrefix, + file: 'EventEmitters.h', +})} +${[...extraIncludes].join('\n')} + +namespace facebook::react { +${events} +} // namespace facebook::react +`; +const ComponentTemplate = ({ + className, + eventName, + structName, + dispatchEventName, + implementation, +}) => { + const capture = implementation.includes('$event') + ? '$event=std::move($event)' + : ''; + return ` +void ${className}EventEmitter::${eventName}(${structName} $event) const { + dispatchEvent("${dispatchEventName}", [${capture}](jsi::Runtime &runtime) { + ${implementation} + }); +} +`; +}; +const BasicComponentTemplate = ({className, eventName, dispatchEventName}) => + ` +void ${className}EventEmitter::${eventName}() const { + dispatchEvent("${dispatchEventName}"); +} +`.trim(); +function generateSetter( + variableName, + propertyName, + propertyParts, + usingEvent, + valueMapper = value => value, +) { + const eventChain = usingEvent + ? `$event.${[...propertyParts, propertyName].join('.')}` + : [propertyParts, propertyName].join('.'); + return `${variableName}.setProperty(runtime, "${propertyName}", ${valueMapper( + eventChain, + )});`; +} +function generateObjectSetter( + variableName, + propertyName, + propertyParts, + typeAnnotation, + extraIncludes, + usingEvent, +) { + return ` +{ + auto ${propertyName} = jsi::Object(runtime); + ${indent( + generateSetters( + propertyName, + typeAnnotation.properties, + propertyParts.concat([propertyName]), + extraIncludes, + usingEvent, + ), + 2, + )} + ${variableName}.setProperty(runtime, "${propertyName}", ${propertyName}); +} +`.trim(); +} +function setValueAtIndex( + propertyName, + indexVariable, + loopLocalVariable, + mappingFunction = value => value, +) { + return `${propertyName}.setValueAtIndex(runtime, ${indexVariable}++, ${mappingFunction( + loopLocalVariable, + )});`; +} +function generateArraySetter( + variableName, + propertyName, + propertyParts, + elementType, + extraIncludes, + usingEvent, +) { + const eventChain = usingEvent + ? `$event.${[...propertyParts, propertyName].join('.')}` + : [propertyParts, propertyName].join('.'); + const indexVar = `${propertyName}Index`; + const innerLoopVar = `${propertyName}Value`; + return ` + auto ${propertyName} = jsi::Array(runtime, ${eventChain}.size()); + size_t ${indexVar} = 0; + for (auto ${innerLoopVar} : ${eventChain}) { + ${handleArrayElementType( + elementType, + propertyName, + indexVar, + innerLoopVar, + propertyParts, + extraIncludes, + usingEvent, + )} + } + ${variableName}.setProperty(runtime, "${propertyName}", ${propertyName}); + `; +} +function handleArrayElementType( + elementType, + propertyName, + indexVariable, + loopLocalVariable, + propertyParts, + extraIncludes, + usingEvent, +) { + switch (elementType.type) { + case 'BooleanTypeAnnotation': + return setValueAtIndex( + propertyName, + indexVariable, + loopLocalVariable, + val => `(bool)${val}`, + ); + case 'StringTypeAnnotation': + case 'Int32TypeAnnotation': + case 'DoubleTypeAnnotation': + case 'FloatTypeAnnotation': + return setValueAtIndex(propertyName, indexVariable, loopLocalVariable); + case 'MixedTypeAnnotation': + return setValueAtIndex( + propertyName, + indexVariable, + loopLocalVariable, + val => `jsi::valueFromDynamic(runtime, ${val})`, + ); + case 'StringEnumTypeAnnotation': + return setValueAtIndex( + propertyName, + indexVariable, + loopLocalVariable, + val => `toString(${val})`, + ); + case 'ObjectTypeAnnotation': + return convertObjectTypeArray( + propertyName, + indexVariable, + loopLocalVariable, + propertyParts, + elementType, + extraIncludes, + ); + case 'ArrayTypeAnnotation': + return convertArrayTypeArray( + propertyName, + indexVariable, + loopLocalVariable, + propertyParts, + elementType, + extraIncludes, + usingEvent, + ); + default: + throw new Error( + `Received invalid elementType for array ${elementType.type}`, + ); + } +} +function convertObjectTypeArray( + propertyName, + indexVariable, + loopLocalVariable, + propertyParts, + objectTypeAnnotation, + extraIncludes, +) { + return `auto ${propertyName}Object = jsi::Object(runtime); + ${generateSetters( + `${propertyName}Object`, + objectTypeAnnotation.properties, + [].concat([loopLocalVariable]), + extraIncludes, + false, + )} + ${setValueAtIndex(propertyName, indexVariable, `${propertyName}Object`)}`; +} +function convertArrayTypeArray( + propertyName, + indexVariable, + loopLocalVariable, + propertyParts, + eventTypeAnnotation, + extraIncludes, + usingEvent, +) { + if (eventTypeAnnotation.type !== 'ArrayTypeAnnotation') { + throw new Error( + `Inconsistent eventTypeAnnotation received. Expected type = 'ArrayTypeAnnotation'; received = ${eventTypeAnnotation.type}`, + ); + } + return `auto ${propertyName}Array = jsi::Array(runtime, ${loopLocalVariable}.size()); + size_t ${indexVariable}Internal = 0; + for (auto ${loopLocalVariable}Internal : ${loopLocalVariable}) { + ${handleArrayElementType( + eventTypeAnnotation.elementType, + `${propertyName}Array`, + `${indexVariable}Internal`, + `${loopLocalVariable}Internal`, + propertyParts, + extraIncludes, + usingEvent, + )} + } + ${setValueAtIndex(propertyName, indexVariable, `${propertyName}Array`)}`; +} +function generateSetters( + parentPropertyName, + properties, + propertyParts, + extraIncludes, + usingEvent = true, +) { + const propSetters = properties + .map(eventProperty => { + const typeAnnotation = eventProperty.typeAnnotation; + switch (typeAnnotation.type) { + case 'BooleanTypeAnnotation': + case 'StringTypeAnnotation': + case 'Int32TypeAnnotation': + case 'DoubleTypeAnnotation': + case 'FloatTypeAnnotation': + return generateSetter( + parentPropertyName, + eventProperty.name, + propertyParts, + usingEvent, + ); + case 'MixedTypeAnnotation': + extraIncludes.add('#include '); + return generateSetter( + parentPropertyName, + eventProperty.name, + propertyParts, + usingEvent, + prop => `jsi::valueFromDynamic(runtime, ${prop})`, + ); + case 'StringEnumTypeAnnotation': + return generateSetter( + parentPropertyName, + eventProperty.name, + propertyParts, + usingEvent, + prop => `toString(${prop})`, + ); + case 'ObjectTypeAnnotation': + return generateObjectSetter( + parentPropertyName, + eventProperty.name, + propertyParts, + typeAnnotation, + extraIncludes, + usingEvent, + ); + case 'ArrayTypeAnnotation': + return generateArraySetter( + parentPropertyName, + eventProperty.name, + propertyParts, + typeAnnotation.elementType, + extraIncludes, + usingEvent, + ); + default: + typeAnnotation.type; + throw new Error( + `Received invalid event property type ${typeAnnotation.type}`, + ); + } + }) + .join('\n'); + return propSetters; +} +function generateEvent(componentName, event, extraIncludes) { + // This is a gross hack necessary because native code is sending + // events named things like topChange to JS which is then converted back to + // call the onChange prop. We should be consistent throughout the system. + // In order to migrate to this new system we have to support the current + // naming scheme. We should delete this once we are able to control this name + // throughout the system. + const dispatchEventName = + event.paperTopLevelNameDeprecated != null + ? event.paperTopLevelNameDeprecated + : `${event.name[2].toLowerCase()}${event.name.slice(3)}`; + if (event.typeAnnotation.argument) { + const implementation = ` + auto $payload = jsi::Object(runtime); + ${generateSetters( + '$payload', + event.typeAnnotation.argument.properties, + [], + extraIncludes, + )} + return $payload; + `.trim(); + if (!event.name.startsWith('on')) { + throw new Error('Expected the event name to start with `on`'); + } + return ComponentTemplate({ + className: componentName, + eventName: event.name, + dispatchEventName, + structName: generateEventStructName([event.name]), + implementation, + }); + } + return BasicComponentTemplate({ + className: componentName, + eventName: event.name, + dispatchEventName, + }); +} +module.exports = { + generate( + libraryName, + schema, + packageName, + assumeNonnull = false, + headerPrefix, + ) { + const moduleComponents = Object.keys(schema.modules) + .map(moduleName => { + const module = schema.modules[moduleName]; + if (module.type !== 'Component') { + return; + } + const components = module.components; + // No components in this module + if (components == null) { + return null; + } + return components; + }) + .filter(Boolean) + .reduce((acc, components) => Object.assign(acc, components), {}); + const extraIncludes = new Set(); + const componentEmitters = Object.keys(moduleComponents) + .map(componentName => { + const component = moduleComponents[componentName]; + return component.events + .map(event => generateEvent(componentName, event, extraIncludes)) + .join('\n'); + }) + .join('\n'); + const fileName = 'EventEmitters.cpp'; + const replacedTemplate = FileTemplate({ + events: componentEmitters, + extraIncludes, + headerPrefix: + headerPrefix !== null && headerPrefix !== void 0 ? headerPrefix : '', + }); + return new Map([[fileName, replacedTemplate]]); + }, +}; diff --git a/packages/react-native-codegen/lib/generators/components/GenerateEventEmitterCpp.js.flow b/packages/react-native-codegen/lib/generators/components/GenerateEventEmitterCpp.js.flow new file mode 100644 index 00000000000000..2f2a829d19955d --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/GenerateEventEmitterCpp.js.flow @@ -0,0 +1,454 @@ +/** + * 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 {EventTypeShape} from '../../CodegenSchema'; +import type { + ComponentShape, + EventTypeAnnotation, + NamedShape, + ObjectTypeAnnotation, + SchemaType, +} from '../../CodegenSchema'; + +const {indent} = require('../Utils'); +const {IncludeTemplate, generateEventStructName} = require('./CppHelpers'); + +// File path -> contents +type FilesOutput = Map; + +type ComponentCollection = $ReadOnly<{ + [component: string]: ComponentShape, + ... +}>; + +const FileTemplate = ({ + events, + extraIncludes, + headerPrefix, +}: { + events: string, + extraIncludes: Set, + headerPrefix: 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: GenerateEventEmitterCpp.js + */ + +${IncludeTemplate({headerPrefix, file: 'EventEmitters.h'})} +${[...extraIncludes].join('\n')} + +namespace facebook::react { +${events} +} // namespace facebook::react +`; + +const ComponentTemplate = ({ + className, + eventName, + structName, + dispatchEventName, + implementation, +}: { + className: string, + eventName: string, + structName: string, + dispatchEventName: string, + implementation: string, +}) => { + const capture = implementation.includes('$event') + ? '$event=std::move($event)' + : ''; + return ` +void ${className}EventEmitter::${eventName}(${structName} $event) const { + dispatchEvent("${dispatchEventName}", [${capture}](jsi::Runtime &runtime) { + ${implementation} + }); +} +`; +}; + +const BasicComponentTemplate = ({ + className, + eventName, + dispatchEventName, +}: { + className: string, + eventName: string, + dispatchEventName: string, +}) => + ` +void ${className}EventEmitter::${eventName}() const { + dispatchEvent("${dispatchEventName}"); +} +`.trim(); + +function generateSetter( + variableName: string, + propertyName: string, + propertyParts: $ReadOnlyArray, + usingEvent: boolean, + valueMapper: string => string = value => value, +) { + const eventChain = usingEvent + ? `$event.${[...propertyParts, propertyName].join('.')}` + : [propertyParts, propertyName].join('.'); + return `${variableName}.setProperty(runtime, "${propertyName}", ${valueMapper( + eventChain, + )});`; +} + +function generateObjectSetter( + variableName: string, + propertyName: string, + propertyParts: $ReadOnlyArray, + typeAnnotation: ObjectTypeAnnotation, + extraIncludes: Set, + usingEvent: boolean, +) { + return ` +{ + auto ${propertyName} = jsi::Object(runtime); + ${indent( + generateSetters( + propertyName, + typeAnnotation.properties, + propertyParts.concat([propertyName]), + extraIncludes, + usingEvent, + ), + 2, + )} + ${variableName}.setProperty(runtime, "${propertyName}", ${propertyName}); +} +`.trim(); +} + +function setValueAtIndex( + propertyName: string, + indexVariable: string, + loopLocalVariable: string, + mappingFunction: string => string = value => value, +) { + return `${propertyName}.setValueAtIndex(runtime, ${indexVariable}++, ${mappingFunction( + loopLocalVariable, + )});`; +} + +function generateArraySetter( + variableName: string, + propertyName: string, + propertyParts: $ReadOnlyArray, + elementType: EventTypeAnnotation, + extraIncludes: Set, + usingEvent: boolean, +): string { + const eventChain = usingEvent + ? `$event.${[...propertyParts, propertyName].join('.')}` + : [propertyParts, propertyName].join('.'); + const indexVar = `${propertyName}Index`; + const innerLoopVar = `${propertyName}Value`; + return ` + auto ${propertyName} = jsi::Array(runtime, ${eventChain}.size()); + size_t ${indexVar} = 0; + for (auto ${innerLoopVar} : ${eventChain}) { + ${handleArrayElementType( + elementType, + propertyName, + indexVar, + innerLoopVar, + propertyParts, + extraIncludes, + usingEvent, + )} + } + ${variableName}.setProperty(runtime, "${propertyName}", ${propertyName}); + `; +} + +function handleArrayElementType( + elementType: EventTypeAnnotation, + propertyName: string, + indexVariable: string, + loopLocalVariable: string, + propertyParts: $ReadOnlyArray, + extraIncludes: Set, + usingEvent: boolean, +): string { + switch (elementType.type) { + case 'BooleanTypeAnnotation': + return setValueAtIndex( + propertyName, + indexVariable, + loopLocalVariable, + val => `(bool)${val}`, + ); + case 'StringTypeAnnotation': + case 'Int32TypeAnnotation': + case 'DoubleTypeAnnotation': + case 'FloatTypeAnnotation': + return setValueAtIndex(propertyName, indexVariable, loopLocalVariable); + case 'MixedTypeAnnotation': + return setValueAtIndex( + propertyName, + indexVariable, + loopLocalVariable, + val => `jsi::valueFromDynamic(runtime, ${val})`, + ); + case 'StringEnumTypeAnnotation': + return setValueAtIndex( + propertyName, + indexVariable, + loopLocalVariable, + val => `toString(${val})`, + ); + case 'ObjectTypeAnnotation': + return convertObjectTypeArray( + propertyName, + indexVariable, + loopLocalVariable, + propertyParts, + elementType, + extraIncludes, + ); + case 'ArrayTypeAnnotation': + return convertArrayTypeArray( + propertyName, + indexVariable, + loopLocalVariable, + propertyParts, + elementType, + extraIncludes, + usingEvent, + ); + default: + throw new Error( + `Received invalid elementType for array ${elementType.type}`, + ); + } +} + +function convertObjectTypeArray( + propertyName: string, + indexVariable: string, + loopLocalVariable: string, + propertyParts: $ReadOnlyArray, + objectTypeAnnotation: ObjectTypeAnnotation, + extraIncludes: Set, +): string { + return `auto ${propertyName}Object = jsi::Object(runtime); + ${generateSetters( + `${propertyName}Object`, + objectTypeAnnotation.properties, + [].concat([loopLocalVariable]), + extraIncludes, + false, + )} + ${setValueAtIndex(propertyName, indexVariable, `${propertyName}Object`)}`; +} + +function convertArrayTypeArray( + propertyName: string, + indexVariable: string, + loopLocalVariable: string, + propertyParts: $ReadOnlyArray, + eventTypeAnnotation: EventTypeAnnotation, + extraIncludes: Set, + usingEvent: boolean, +): string { + if (eventTypeAnnotation.type !== 'ArrayTypeAnnotation') { + throw new Error( + `Inconsistent eventTypeAnnotation received. Expected type = 'ArrayTypeAnnotation'; received = ${eventTypeAnnotation.type}`, + ); + } + return `auto ${propertyName}Array = jsi::Array(runtime, ${loopLocalVariable}.size()); + size_t ${indexVariable}Internal = 0; + for (auto ${loopLocalVariable}Internal : ${loopLocalVariable}) { + ${handleArrayElementType( + eventTypeAnnotation.elementType, + `${propertyName}Array`, + `${indexVariable}Internal`, + `${loopLocalVariable}Internal`, + propertyParts, + extraIncludes, + usingEvent, + )} + } + ${setValueAtIndex(propertyName, indexVariable, `${propertyName}Array`)}`; +} + +function generateSetters( + parentPropertyName: string, + properties: $ReadOnlyArray>, + propertyParts: $ReadOnlyArray, + extraIncludes: Set, + usingEvent: boolean = true, +): string { + const propSetters = properties + .map(eventProperty => { + const {typeAnnotation} = eventProperty; + switch (typeAnnotation.type) { + case 'BooleanTypeAnnotation': + case 'StringTypeAnnotation': + case 'Int32TypeAnnotation': + case 'DoubleTypeAnnotation': + case 'FloatTypeAnnotation': + return generateSetter( + parentPropertyName, + eventProperty.name, + propertyParts, + usingEvent, + ); + case 'MixedTypeAnnotation': + extraIncludes.add('#include '); + return generateSetter( + parentPropertyName, + eventProperty.name, + propertyParts, + usingEvent, + prop => `jsi::valueFromDynamic(runtime, ${prop})`, + ); + case 'StringEnumTypeAnnotation': + return generateSetter( + parentPropertyName, + eventProperty.name, + propertyParts, + usingEvent, + prop => `toString(${prop})`, + ); + case 'ObjectTypeAnnotation': + return generateObjectSetter( + parentPropertyName, + eventProperty.name, + propertyParts, + typeAnnotation, + extraIncludes, + usingEvent, + ); + case 'ArrayTypeAnnotation': + return generateArraySetter( + parentPropertyName, + eventProperty.name, + propertyParts, + typeAnnotation.elementType, + extraIncludes, + usingEvent, + ); + default: + (typeAnnotation.type: empty); + throw new Error( + `Received invalid event property type ${typeAnnotation.type}`, + ); + } + }) + .join('\n'); + + return propSetters; +} + +function generateEvent( + componentName: string, + event: EventTypeShape, + extraIncludes: Set, +): string { + // This is a gross hack necessary because native code is sending + // events named things like topChange to JS which is then converted back to + // call the onChange prop. We should be consistent throughout the system. + // In order to migrate to this new system we have to support the current + // naming scheme. We should delete this once we are able to control this name + // throughout the system. + const dispatchEventName = + event.paperTopLevelNameDeprecated != null + ? event.paperTopLevelNameDeprecated + : `${event.name[2].toLowerCase()}${event.name.slice(3)}`; + + if (event.typeAnnotation.argument) { + const implementation = ` + auto $payload = jsi::Object(runtime); + ${generateSetters( + '$payload', + event.typeAnnotation.argument.properties, + [], + extraIncludes, + )} + return $payload; + `.trim(); + + if (!event.name.startsWith('on')) { + throw new Error('Expected the event name to start with `on`'); + } + + return ComponentTemplate({ + className: componentName, + eventName: event.name, + dispatchEventName, + structName: generateEventStructName([event.name]), + implementation, + }); + } + + return BasicComponentTemplate({ + className: componentName, + eventName: event.name, + dispatchEventName, + }); +} + +module.exports = { + generate( + libraryName: string, + schema: SchemaType, + packageName?: string, + assumeNonnull: boolean = false, + headerPrefix?: string, + ): FilesOutput { + const moduleComponents: ComponentCollection = 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 components; + }) + .filter(Boolean) + .reduce((acc, components) => Object.assign(acc, components), {}); + + const extraIncludes = new Set(); + const componentEmitters = Object.keys(moduleComponents) + .map(componentName => { + const component = moduleComponents[componentName]; + return component.events + .map(event => generateEvent(componentName, event, extraIncludes)) + .join('\n'); + }) + .join('\n'); + + const fileName = 'EventEmitters.cpp'; + const replacedTemplate = FileTemplate({ + events: componentEmitters, + extraIncludes, + headerPrefix: headerPrefix ?? '', + }); + + return new Map([[fileName, replacedTemplate]]); + }, +}; diff --git a/packages/react-native-codegen/lib/generators/components/GenerateEventEmitterH.js b/packages/react-native-codegen/lib/generators/components/GenerateEventEmitterH.js new file mode 100644 index 00000000000000..41a8d4ed377291 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/GenerateEventEmitterH.js @@ -0,0 +1,280 @@ +/** + * 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 _require = require('../Utils'), + indent = _require.indent, + toSafeCppString = _require.toSafeCppString; +const _require2 = require('./CppHelpers'), + generateEventStructName = _require2.generateEventStructName, + getCppArrayTypeForAnnotation = _require2.getCppArrayTypeForAnnotation, + getCppTypeForAnnotation = _require2.getCppTypeForAnnotation, + getImports = _require2.getImports; +const nullthrows = require('nullthrows'); + +// File path -> contents + +const FileTemplate = ({componentEmitters, extraIncludes}) => ` +/** + * 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: GenerateEventEmitterH.js + */ +#pragma once + +#include +${[...extraIncludes].join('\n')} + +namespace facebook::react { +${componentEmitters} +} // namespace facebook::react +`; +const ComponentTemplate = ({className, structs, events}) => + ` +class ${className}EventEmitter : public ViewEventEmitter { + public: + using ViewEventEmitter::ViewEventEmitter; + + ${structs} + ${events} +}; +`.trim(); +const StructTemplate = ({structName, fields}) => + ` + struct ${structName} { + ${fields} + }; +`.trim(); +const EnumTemplate = ({enumName, values, toCases}) => + `enum class ${enumName} { + ${values} +}; + +static char const *toString(const ${enumName} value) { + switch (value) { + ${toCases} + } +} +`.trim(); +function getNativeTypeFromAnnotation(componentName, eventProperty, nameParts) { + const type = eventProperty.typeAnnotation.type; + switch (type) { + case 'BooleanTypeAnnotation': + case 'StringTypeAnnotation': + case 'Int32TypeAnnotation': + case 'DoubleTypeAnnotation': + case 'FloatTypeAnnotation': + case 'MixedTypeAnnotation': + return getCppTypeForAnnotation(type); + case 'StringEnumTypeAnnotation': + case 'ObjectTypeAnnotation': + return generateEventStructName([...nameParts, eventProperty.name]); + case 'ArrayTypeAnnotation': + const eventTypeAnnotation = eventProperty.typeAnnotation; + if (eventTypeAnnotation.type !== 'ArrayTypeAnnotation') { + throw new Error( + "Inconsistent Codegen state: type was ArrayTypeAnnotation at the beginning of the body and now it isn't", + ); + } + return getCppArrayTypeForAnnotation(eventTypeAnnotation.elementType, [ + ...nameParts, + eventProperty.name, + ]); + default: + type; + throw new Error(`Received invalid event property type ${type}`); + } +} +function generateEnum(structs, options, nameParts) { + const structName = generateEventStructName(nameParts); + const fields = options + .map((option, index) => `${toSafeCppString(option)}`) + .join(',\n '); + const toCases = options + .map( + option => + `case ${structName}::${toSafeCppString(option)}: return "${option}";`, + ) + .join('\n' + ' '); + structs.set( + structName, + EnumTemplate({ + enumName: structName, + values: fields, + toCases: toCases, + }), + ); +} +function handleGenerateStructForArray( + structs, + name, + componentName, + elementType, + nameParts, +) { + if (elementType.type === 'ObjectTypeAnnotation') { + generateStruct( + structs, + componentName, + nameParts.concat([name]), + nullthrows(elementType.properties), + ); + } else if (elementType.type === 'StringEnumTypeAnnotation') { + generateEnum(structs, elementType.options, nameParts.concat([name])); + } else if (elementType.type === 'ArrayTypeAnnotation') { + handleGenerateStructForArray( + structs, + name, + componentName, + elementType.elementType, + nameParts, + ); + } +} +function generateStruct(structs, componentName, nameParts, properties) { + const structNameParts = nameParts; + const structName = generateEventStructName(structNameParts); + const fields = properties + .map(property => { + return `${getNativeTypeFromAnnotation( + componentName, + property, + structNameParts, + )} ${property.name};`; + }) + .join('\n' + ' '); + properties.forEach(property => { + const name = property.name, + typeAnnotation = property.typeAnnotation; + switch (typeAnnotation.type) { + case 'BooleanTypeAnnotation': + case 'StringTypeAnnotation': + case 'Int32TypeAnnotation': + case 'DoubleTypeAnnotation': + case 'FloatTypeAnnotation': + case 'MixedTypeAnnotation': + return; + case 'ArrayTypeAnnotation': + handleGenerateStructForArray( + structs, + name, + componentName, + typeAnnotation.elementType, + nameParts, + ); + return; + case 'ObjectTypeAnnotation': + generateStruct( + structs, + componentName, + nameParts.concat([name]), + nullthrows(typeAnnotation.properties), + ); + return; + case 'StringEnumTypeAnnotation': + generateEnum(structs, typeAnnotation.options, nameParts.concat([name])); + return; + default: + typeAnnotation.type; + throw new Error( + `Received invalid event property type ${typeAnnotation.type}`, + ); + } + }); + structs.set( + structName, + StructTemplate({ + structName, + fields, + }), + ); +} +function generateStructs(componentName, component) { + const structs = new Map(); + component.events.forEach(event => { + if (event.typeAnnotation.argument) { + generateStruct( + structs, + componentName, + [event.name], + event.typeAnnotation.argument.properties, + ); + } + }); + return Array.from(structs.values()).join('\n\n'); +} +function generateEvent(componentName, event) { + if (event.typeAnnotation.argument) { + const structName = generateEventStructName([event.name]); + return `void ${event.name}(${structName} value) const;`; + } + return `void ${event.name}() const;`; +} +function generateEvents(componentName, component) { + return component.events + .map(event => generateEvent(componentName, event)) + .join('\n\n' + ' '); +} +module.exports = { + generate( + libraryName, + schema, + packageName, + assumeNonnull = false, + headerPrefix, + ) { + const moduleComponents = Object.keys(schema.modules) + .map(moduleName => { + const module = schema.modules[moduleName]; + if (module.type !== 'Component') { + return null; + } + const components = module.components; + // No components in this module + if (components == null) { + return null; + } + return components; + }) + .filter(Boolean) + .reduce((acc, components) => Object.assign(acc, components), {}); + const extraIncludes = new Set(); + const componentEmitters = Object.keys(moduleComponents) + .map(componentName => { + const component = moduleComponents[componentName]; + component.events.forEach(event => { + if (event.typeAnnotation.argument) { + const argIncludes = getImports( + event.typeAnnotation.argument.properties, + ); + // $FlowFixMe[method-unbinding] + argIncludes.forEach(extraIncludes.add, extraIncludes); + } + }); + const replacedTemplate = ComponentTemplate({ + className: componentName, + structs: indent(generateStructs(componentName, component), 2), + events: generateEvents(componentName, component), + }); + return replacedTemplate; + }) + .join('\n'); + const fileName = 'EventEmitters.h'; + const replacedTemplate = FileTemplate({ + componentEmitters, + extraIncludes, + }); + return new Map([[fileName, replacedTemplate]]); + }, +}; diff --git a/packages/react-native-codegen/lib/generators/components/GenerateEventEmitterH.js.flow b/packages/react-native-codegen/lib/generators/components/GenerateEventEmitterH.js.flow new file mode 100644 index 00000000000000..0e753e24d2c544 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/GenerateEventEmitterH.js.flow @@ -0,0 +1,367 @@ +/** + * 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-local + * @format + */ + +'use strict'; + +import type { + ComponentShape, + EventTypeAnnotation, + EventTypeShape, + NamedShape, + SchemaType, +} from '../../CodegenSchema'; + +const {indent, toSafeCppString} = require('../Utils'); +const { + generateEventStructName, + getCppArrayTypeForAnnotation, + getCppTypeForAnnotation, + getImports, +} = require('./CppHelpers'); +const nullthrows = require('nullthrows'); + +// File path -> contents +type FilesOutput = Map; +type StructsMap = Map; + +type ComponentCollection = $ReadOnly<{ + [component: string]: ComponentShape, + ... +}>; + +const FileTemplate = ({ + componentEmitters, + extraIncludes, +}: { + componentEmitters: string, + extraIncludes: Set, +}) => ` +/** + * 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: GenerateEventEmitterH.js + */ +#pragma once + +#include +${[...extraIncludes].join('\n')} + +namespace facebook::react { +${componentEmitters} +} // namespace facebook::react +`; + +const ComponentTemplate = ({ + className, + structs, + events, +}: { + className: string, + structs: string, + events: string, +}) => + ` +class ${className}EventEmitter : public ViewEventEmitter { + public: + using ViewEventEmitter::ViewEventEmitter; + + ${structs} + ${events} +}; +`.trim(); + +const StructTemplate = ({ + structName, + fields, +}: { + structName: string, + fields: string, +}) => + ` + struct ${structName} { + ${fields} + }; +`.trim(); + +const EnumTemplate = ({ + enumName, + values, + toCases, +}: { + enumName: string, + values: string, + toCases: string, +}) => + `enum class ${enumName} { + ${values} +}; + +static char const *toString(const ${enumName} value) { + switch (value) { + ${toCases} + } +} +`.trim(); + +function getNativeTypeFromAnnotation( + componentName: string, + eventProperty: NamedShape, + nameParts: $ReadOnlyArray, +): string { + const {type} = eventProperty.typeAnnotation; + + switch (type) { + case 'BooleanTypeAnnotation': + case 'StringTypeAnnotation': + case 'Int32TypeAnnotation': + case 'DoubleTypeAnnotation': + case 'FloatTypeAnnotation': + case 'MixedTypeAnnotation': + return getCppTypeForAnnotation(type); + case 'StringEnumTypeAnnotation': + case 'ObjectTypeAnnotation': + return generateEventStructName([...nameParts, eventProperty.name]); + case 'ArrayTypeAnnotation': + const eventTypeAnnotation = eventProperty.typeAnnotation; + if (eventTypeAnnotation.type !== 'ArrayTypeAnnotation') { + throw new Error( + "Inconsistent Codegen state: type was ArrayTypeAnnotation at the beginning of the body and now it isn't", + ); + } + return getCppArrayTypeForAnnotation(eventTypeAnnotation.elementType, [ + ...nameParts, + eventProperty.name, + ]); + default: + (type: empty); + throw new Error(`Received invalid event property type ${type}`); + } +} +function generateEnum( + structs: StructsMap, + options: $ReadOnlyArray, + nameParts: Array, +) { + const structName = generateEventStructName(nameParts); + const fields = options + .map((option, index) => `${toSafeCppString(option)}`) + .join(',\n '); + + const toCases = options + .map( + option => + `case ${structName}::${toSafeCppString(option)}: return "${option}";`, + ) + .join('\n' + ' '); + + structs.set( + structName, + EnumTemplate({ + enumName: structName, + values: fields, + toCases: toCases, + }), + ); +} + +function handleGenerateStructForArray( + structs: StructsMap, + name: string, + componentName: string, + elementType: EventTypeAnnotation, + nameParts: $ReadOnlyArray, +): void { + if (elementType.type === 'ObjectTypeAnnotation') { + generateStruct( + structs, + componentName, + nameParts.concat([name]), + nullthrows(elementType.properties), + ); + } else if (elementType.type === 'StringEnumTypeAnnotation') { + generateEnum(structs, elementType.options, nameParts.concat([name])); + } else if (elementType.type === 'ArrayTypeAnnotation') { + handleGenerateStructForArray( + structs, + name, + componentName, + elementType.elementType, + nameParts, + ); + } +} + +function generateStruct( + structs: StructsMap, + componentName: string, + nameParts: $ReadOnlyArray, + properties: $ReadOnlyArray>, +): void { + const structNameParts = nameParts; + const structName = generateEventStructName(structNameParts); + + const fields = properties + .map(property => { + return `${getNativeTypeFromAnnotation( + componentName, + property, + structNameParts, + )} ${property.name};`; + }) + .join('\n' + ' '); + + properties.forEach(property => { + const {name, typeAnnotation} = property; + switch (typeAnnotation.type) { + case 'BooleanTypeAnnotation': + case 'StringTypeAnnotation': + case 'Int32TypeAnnotation': + case 'DoubleTypeAnnotation': + case 'FloatTypeAnnotation': + case 'MixedTypeAnnotation': + return; + case 'ArrayTypeAnnotation': + handleGenerateStructForArray( + structs, + name, + componentName, + typeAnnotation.elementType, + nameParts, + ); + return; + case 'ObjectTypeAnnotation': + generateStruct( + structs, + componentName, + nameParts.concat([name]), + nullthrows(typeAnnotation.properties), + ); + return; + case 'StringEnumTypeAnnotation': + generateEnum(structs, typeAnnotation.options, nameParts.concat([name])); + return; + default: + (typeAnnotation.type: empty); + throw new Error( + `Received invalid event property type ${typeAnnotation.type}`, + ); + } + }); + + structs.set( + structName, + StructTemplate({ + structName, + fields, + }), + ); +} + +function generateStructs( + componentName: string, + component: ComponentShape, +): string { + const structs: StructsMap = new Map(); + + component.events.forEach(event => { + if (event.typeAnnotation.argument) { + generateStruct( + structs, + componentName, + [event.name], + event.typeAnnotation.argument.properties, + ); + } + }); + + return Array.from(structs.values()).join('\n\n'); +} + +function generateEvent(componentName: string, event: EventTypeShape): string { + if (event.typeAnnotation.argument) { + const structName = generateEventStructName([event.name]); + + return `void ${event.name}(${structName} value) const;`; + } + + return `void ${event.name}() const;`; +} +function generateEvents( + componentName: string, + component: ComponentShape, +): string { + return component.events + .map(event => generateEvent(componentName, event)) + .join('\n\n' + ' '); +} + +module.exports = { + generate( + libraryName: string, + schema: SchemaType, + packageName?: string, + assumeNonnull: boolean = false, + headerPrefix?: string, + ): FilesOutput { + const moduleComponents: ComponentCollection = Object.keys(schema.modules) + .map(moduleName => { + const module = schema.modules[moduleName]; + if (module.type !== 'Component') { + return null; + } + + const {components} = module; + // No components in this module + if (components == null) { + return null; + } + + return components; + }) + .filter(Boolean) + .reduce((acc, components) => Object.assign(acc, components), {}); + + const extraIncludes = new Set(); + const componentEmitters = Object.keys(moduleComponents) + .map(componentName => { + const component = moduleComponents[componentName]; + + component.events.forEach(event => { + if (event.typeAnnotation.argument) { + const argIncludes = getImports( + event.typeAnnotation.argument.properties, + ); + // $FlowFixMe[method-unbinding] + argIncludes.forEach(extraIncludes.add, extraIncludes); + } + }); + + const replacedTemplate = ComponentTemplate({ + className: componentName, + structs: indent(generateStructs(componentName, component), 2), + events: generateEvents(componentName, component), + }); + + return replacedTemplate; + }) + .join('\n'); + + const fileName = 'EventEmitters.h'; + const replacedTemplate = FileTemplate({ + componentEmitters, + extraIncludes, + }); + + return new Map([[fileName, replacedTemplate]]); + }, +}; diff --git a/packages/react-native-codegen/lib/generators/components/GeneratePropsCpp.js b/packages/react-native-codegen/lib/generators/components/GeneratePropsCpp.js new file mode 100644 index 00000000000000..c5f213342ea66c --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/GeneratePropsCpp.js @@ -0,0 +1,143 @@ +/** + * 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 _require = require('./CppHelpers'), + IncludeTemplate = _require.IncludeTemplate, + convertDefaultTypeToString = _require.convertDefaultTypeToString, + getImports = _require.getImports, + getSourceProp = _require.getSourceProp, + isWrappedPropType = _require.isWrappedPropType; + +// File path -> contents + +const FileTemplate = ({imports, componentClasses, headerPrefix}) => ` +/** + * 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: GeneratePropsCpp.js + */ + +${IncludeTemplate({ + headerPrefix, + file: 'Props.h', +})} +${imports} + +namespace facebook::react { + +${componentClasses} + +} // namespace facebook::react +`; +const ComponentTemplate = ({className, extendClasses, props}) => + ` +${className}::${className}( + const PropsParserContext &context, + const ${className} &sourceProps, + const RawProps &rawProps):${extendClasses} + + ${props} + {} +`.trim(); +function generatePropsString(componentName, component) { + return component.props + .map(prop => { + const sourceProp = getSourceProp(componentName, prop); + const defaultValue = convertDefaultTypeToString(componentName, prop); + const isWrappedProp = isWrappedPropType(prop); + let convertRawProp = `convertRawProp(context, rawProps, "${prop.name}", ${sourceProp}, {${defaultValue}})`; + if (isWrappedProp) { + convertRawProp += '.value'; + } + return `${prop.name}(${convertRawProp})`; + }) + .join(',\n' + ' '); +} +function getClassExtendString(component) { + const extendString = + ' ' + + component.extendsProps + .map(extendProps => { + switch (extendProps.type) { + case 'ReactNativeBuiltInType': + switch (extendProps.knownTypeName) { + case 'ReactNativeCoreViewProps': + return 'ViewProps(context, sourceProps, rawProps)'; + default: + extendProps.knownTypeName; + throw new Error('Invalid knownTypeName'); + } + default: + extendProps.type; + throw new Error('Invalid extended type'); + } + }) + .join(', ') + + `${component.props.length > 0 ? ',' : ''}`; + return extendString; +} +module.exports = { + generate( + libraryName, + schema, + packageName, + assumeNonnull = false, + headerPrefix, + ) { + const fileName = 'Props.cpp'; + const allImports = new Set([ + '#include ', + '#include ', + ]); + const componentProps = Object.keys(schema.modules) + .map(moduleName => { + const module = schema.modules[moduleName]; + if (module.type !== 'Component') { + return; + } + const components = module.components; + // 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 propsString = generatePropsString(componentName, component); + const extendString = getClassExtendString(component); + const imports = getImports(component.props); + // $FlowFixMe[method-unbinding] added when improving typing for this parameters + imports.forEach(allImports.add, allImports); + const replacedTemplate = ComponentTemplate({ + className: newName, + extendClasses: extendString, + props: propsString, + }); + return replacedTemplate; + }) + .join('\n'); + }) + .filter(Boolean) + .join('\n'); + const replacedTemplate = FileTemplate({ + componentClasses: componentProps, + imports: Array.from(allImports).sort().join('\n').trim(), + headerPrefix: + headerPrefix !== null && headerPrefix !== void 0 ? headerPrefix : '', + }); + return new Map([[fileName, replacedTemplate]]); + }, +}; diff --git a/packages/react-native-codegen/lib/generators/components/GeneratePropsCpp.js.flow b/packages/react-native-codegen/lib/generators/components/GeneratePropsCpp.js.flow new file mode 100644 index 00000000000000..7012ab8f55169c --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/GeneratePropsCpp.js.flow @@ -0,0 +1,173 @@ +/** + * 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, SchemaType} from '../../CodegenSchema'; + +const { + IncludeTemplate, + convertDefaultTypeToString, + getImports, + getSourceProp, + isWrappedPropType, +} = require('./CppHelpers'); + +// File path -> contents +type FilesOutput = Map; + +const FileTemplate = ({ + imports, + componentClasses, + headerPrefix, +}: { + imports: string, + componentClasses: string, + headerPrefix: 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: GeneratePropsCpp.js + */ + +${IncludeTemplate({headerPrefix, file: 'Props.h'})} +${imports} + +namespace facebook::react { + +${componentClasses} + +} // namespace facebook::react +`; + +const ComponentTemplate = ({ + className, + extendClasses, + props, +}: { + className: string, + extendClasses: string, + props: string, +}) => + ` +${className}::${className}( + const PropsParserContext &context, + const ${className} &sourceProps, + const RawProps &rawProps):${extendClasses} + + ${props} + {} +`.trim(); + +function generatePropsString(componentName: string, component: ComponentShape) { + return component.props + .map(prop => { + const sourceProp = getSourceProp(componentName, prop); + const defaultValue = convertDefaultTypeToString(componentName, prop); + const isWrappedProp = isWrappedPropType(prop); + let convertRawProp = `convertRawProp(context, rawProps, "${prop.name}", ${sourceProp}, {${defaultValue}})`; + if (isWrappedProp) { + convertRawProp += '.value'; + } + return `${prop.name}(${convertRawProp})`; + }) + .join(',\n' + ' '); +} + +function getClassExtendString(component: ComponentShape): string { + const extendString = + ' ' + + component.extendsProps + .map(extendProps => { + switch (extendProps.type) { + case 'ReactNativeBuiltInType': + switch (extendProps.knownTypeName) { + case 'ReactNativeCoreViewProps': + return 'ViewProps(context, sourceProps, rawProps)'; + default: + (extendProps.knownTypeName: empty); + throw new Error('Invalid knownTypeName'); + } + default: + (extendProps.type: empty); + throw new Error('Invalid extended type'); + } + }) + .join(', ') + + `${component.props.length > 0 ? ',' : ''}`; + + return extendString; +} + +module.exports = { + generate( + libraryName: string, + schema: SchemaType, + packageName?: string, + assumeNonnull: boolean = false, + headerPrefix?: string, + ): FilesOutput { + const fileName = 'Props.cpp'; + const allImports: Set = new Set([ + '#include ', + '#include ', + ]); + + const componentProps = 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 propsString = generatePropsString(componentName, component); + const extendString = getClassExtendString(component); + + const imports = getImports(component.props); + // $FlowFixMe[method-unbinding] added when improving typing for this parameters + imports.forEach(allImports.add, allImports); + + const replacedTemplate = ComponentTemplate({ + className: newName, + extendClasses: extendString, + props: propsString, + }); + + return replacedTemplate; + }) + .join('\n'); + }) + .filter(Boolean) + .join('\n'); + + const replacedTemplate = FileTemplate({ + componentClasses: componentProps, + imports: Array.from(allImports).sort().join('\n').trim(), + headerPrefix: headerPrefix ?? '', + }); + + return new Map([[fileName, replacedTemplate]]); + }, +}; diff --git a/packages/react-native-codegen/lib/generators/components/GeneratePropsH.js b/packages/react-native-codegen/lib/generators/components/GeneratePropsH.js new file mode 100644 index 00000000000000..32c83ebde1a826 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/GeneratePropsH.js @@ -0,0 +1,621 @@ +/** + * 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 _require = require('../Utils'), + getEnumName = _require.getEnumName, + toSafeCppString = _require.toSafeCppString; +const _require2 = require('./ComponentsGeneratorUtils.js'), + getLocalImports = _require2.getLocalImports, + getNativeTypeFromAnnotation = _require2.getNativeTypeFromAnnotation; +const _require3 = require('./CppHelpers.js'), + generateStructName = _require3.generateStructName, + getDefaultInitializerString = _require3.getDefaultInitializerString, + getEnumMaskName = _require3.getEnumMaskName, + toIntEnumValueName = _require3.toIntEnumValueName; + +// File path -> contents + +const FileTemplate = ({imports, componentClasses}) => ` +/** + * 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::react { + +${componentClasses} + +} // namespace facebook::react +`; +const ClassTemplate = ({enums, structs, className, props, extendClasses}) => + ` +${enums} +${structs} +class ${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}) => + ` +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}) => + ` +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}) => + `struct ${structName} { + ${fields} +}; + +static inline void fromRawValue(const PropsParserContext& context, const RawValue &value, ${structName} &result) { + auto map = (std::unordered_map)value; + + ${fromCases} +} + +static inline std::string toString(const ${structName} &value) { + return "[Object ${structName}]"; +} +`.trim(); +const ArrayConversionFunctionTemplate = ({ + structName, +}) => `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, +}) => `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}) => + ` +using ${enumMask} = uint32_t; + +struct ${enumMask}Wrapped { + ${enumMask} value; +}; + +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}Wrapped &wrapped) { + auto items = std::vector{value}; + for (const auto &item : items) { + ${fromCases} + abort(); + } +} + +static inline std::string toString(const ${enumMask}Wrapped &wrapped) { + auto result = std::string{}; + auto separator = std::string{", "}; + + ${toCases} + if (!result.empty()) { + result.erase(result.length() - separator.length()); + } + return result; +} +`.trim(); +function getClassExtendString(component) { + 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; + throw new Error('Invalid knownTypeName'); + } + default: + extendProps.type; + throw new Error('Invalid extended type'); + } + }) + .join(' '); + return extendString; +} +function convertValueToEnumOption(value) { + return toSafeCppString(value); +} +function generateArrayEnumString(componentName, name, options) { + 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}") { + wrapped.value |= ${enumName}::${toSafeCppString(option)}; + continue; + }`, + ) + .join('\n '); + const toCases = options + .map( + option => `if (wrapped.value & ${enumName}::${toSafeCppString(option)}) { + result += "${option}" + separator; + }`, + ) + .join('\n' + ' '); + return ArrayEnumTemplate({ + enumName, + enumMask: getEnumMaskName(enumName), + values, + fromCases, + toCases, + }); +} +function generateStringEnum(componentName, prop) { + const typeAnnotation = prop.typeAnnotation; + if (typeAnnotation.type === 'StringEnumTypeAnnotation') { + const values = 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, prop) { + const typeAnnotation = prop.typeAnnotation; + if (typeAnnotation.type === 'Int32EnumTypeAnnotation') { + const values = 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, component) { + 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, props, nameParts) { + return props + .map(prop => { + const nativeType = getNativeTypeFromAnnotation( + componentName, + prop, + nameParts, + ); + const defaultInitializer = getDefaultInitializerString( + componentName, + prop, + ); + return `${nativeType} ${prop.name}${defaultInitializer};`; + }) + .join('\n' + ' '); +} +function getExtendsImports(extendsProps) { + const imports = new Set(); + imports.add('#include '); + extendsProps.forEach(extendProps => { + switch (extendProps.type) { + case 'ReactNativeBuiltInType': + switch (extendProps.knownTypeName) { + case 'ReactNativeCoreViewProps': + imports.add( + '#include ', + ); + return; + default: + extendProps.knownTypeName; + throw new Error('Invalid knownTypeName'); + } + default: + extendProps.type; + throw new Error('Invalid extended type'); + } + }); + return imports; +} +function generateStructsForComponent(componentName, component) { + 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, properties, nameParts) { + const structs = 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, componentName, nameParts, properties) { + const structNameParts = nameParts; + const structName = generateStructName(componentName, structNameParts); + const fields = generatePropsString( + componentName, + properties, + structNameParts, + ); + properties.forEach(property => { + 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; + 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, + schema, + packageName, + assumeNonnull = false, + headerPrefix, + ) { + const fileName = 'Props.h'; + const allImports = new Set(); + const componentClasses = Object.keys(schema.modules) + .map(moduleName => { + const module = schema.modules[moduleName]; + if (module.type !== 'Component') { + return; + } + const components = module.components; + // 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]]); + }, +}; diff --git a/packages/react-native-codegen/lib/generators/components/GeneratePropsH.js.flow b/packages/react-native-codegen/lib/generators/components/GeneratePropsH.js.flow new file mode 100644 index 00000000000000..a623ebe2db4cf8 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/GeneratePropsH.js.flow @@ -0,0 +1,781 @@ +/** + * 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'; +import type { + ExtendsPropsShape, + NamedShape, + PropTypeAnnotation, + SchemaType, +} from '../../CodegenSchema'; + +const {getEnumName, toSafeCppString} = require('../Utils'); +const { + getLocalImports, + getNativeTypeFromAnnotation, +} = require('./ComponentsGeneratorUtils.js'); +const { + generateStructName, + getDefaultInitializerString, + getEnumMaskName, + toIntEnumValueName, +} = require('./CppHelpers.js'); + +// 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::react { + +${componentClasses} + +} // namespace facebook::react +`; + +const ClassTemplate = ({ + enums, + structs, + className, + props, + extendClasses, +}: { + enums: string, + structs: string, + className: string, + props: string, + extendClasses: string, +}) => + ` +${enums} +${structs} +class ${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 = (std::unordered_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; + +struct ${enumMask}Wrapped { + ${enumMask} value; +}; + +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}Wrapped &wrapped) { + auto items = std::vector{value}; + for (const auto &item : items) { + ${fromCases} + abort(); + } +} + +static inline std::string toString(const ${enumMask}Wrapped &wrapped) { + 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}") { + wrapped.value |= ${enumName}::${toSafeCppString(option)}; + continue; + }`, + ) + .join('\n '); + + const toCases = options + .map( + option => + `if (wrapped.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>, + nameParts: $ReadOnlyArray, +) { + return props + .map(prop => { + const nativeType = getNativeTypeFromAnnotation( + componentName, + prop, + nameParts, + ); + const defaultInitializer = getDefaultInitializerString( + componentName, + prop, + ); + + return `${nativeType} ${prop.name}${defaultInitializer};`; + }) + .join('\n' + ' '); +} + +function getExtendsImports( + extendsProps: $ReadOnlyArray, +): Set { + const imports: Set = new Set(); + + 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 = generatePropsString( + componentName, + properties, + structNameParts, + ); + + 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, + headerPrefix?: string, + ): 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]]); + }, +}; diff --git a/packages/react-native-codegen/lib/generators/components/GeneratePropsJavaDelegate.js b/packages/react-native-codegen/lib/generators/components/GeneratePropsJavaDelegate.js new file mode 100644 index 00000000000000..21119466548071 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/GeneratePropsJavaDelegate.js @@ -0,0 +1,299 @@ +/** + * 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 _require = require('./JavaHelpers'), + getDelegateJavaClassName = _require.getDelegateJavaClassName, + getImports = _require.getImports, + getInterfaceJavaClassName = _require.getInterfaceJavaClassName, + toSafeJavaString = _require.toSafeJavaString; + +// File path -> contents + +const FileTemplate = ({ + packageName, + imports, + className, + extendClasses, + interfaceClassName, + methods, +}) => `/** +* 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: GeneratePropsJavaDelegate.js +*/ + +package ${packageName}; + +${imports} + +public class ${className} & ${interfaceClassName}> extends BaseViewManagerDelegate { + public ${className}(U viewManager) { + super(viewManager); + } + ${methods} +} +`; +const PropSetterTemplate = ({propCases}) => + ` + @Override + public void setProperty(T view, String propName, @Nullable Object value) { + ${propCases} + } +`.trim(); +const CommandsTemplate = ({commandCases}) => + ` + @Override + public void receiveCommand(T view, String commandName, @Nullable ReadableArray args) { + switch (commandName) { + ${commandCases} + } + } +`.trim(); +function getJavaValueForProp(prop, componentName) { + const typeAnnotation = prop.typeAnnotation; + switch (typeAnnotation.type) { + case 'BooleanTypeAnnotation': + if (typeAnnotation.default === null) { + return 'value == null ? null : (Boolean) value'; + } else { + return `value == null ? ${typeAnnotation.default.toString()} : (boolean) value`; + } + case 'StringTypeAnnotation': + const defaultValueString = + typeAnnotation.default === null + ? 'null' + : `"${typeAnnotation.default}"`; + return `value == null ? ${defaultValueString} : (String) value`; + case 'Int32TypeAnnotation': + return `value == null ? ${typeAnnotation.default} : ((Double) value).intValue()`; + case 'DoubleTypeAnnotation': + if (prop.optional) { + return `value == null ? ${typeAnnotation.default}f : ((Double) value).doubleValue()`; + } else { + return 'value == null ? Double.NaN : ((Double) value).doubleValue()'; + } + case 'FloatTypeAnnotation': + if (typeAnnotation.default === null) { + return 'value == null ? null : ((Double) value).floatValue()'; + } else if (prop.optional) { + return `value == null ? ${typeAnnotation.default}f : ((Double) value).floatValue()`; + } else { + return 'value == null ? Float.NaN : ((Double) value).floatValue()'; + } + case 'ReservedPropTypeAnnotation': + switch (typeAnnotation.name) { + case 'ColorPrimitive': + return 'ColorPropConverter.getColor(value, view.getContext())'; + case 'ImageSourcePrimitive': + return '(ReadableMap) value'; + case 'ImageRequestPrimitive': + return '(ReadableMap) value'; + case 'PointPrimitive': + return '(ReadableMap) value'; + case 'EdgeInsetsPrimitive': + return '(ReadableMap) value'; + case 'DimensionPrimitive': + return 'DimensionPropConverter.getDimension(value)'; + default: + typeAnnotation.name; + throw new Error('Received unknown ReservedPropTypeAnnotation'); + } + case 'ArrayTypeAnnotation': { + return '(ReadableArray) value'; + } + case 'ObjectTypeAnnotation': { + return '(ReadableMap) value'; + } + case 'StringEnumTypeAnnotation': + return '(String) value'; + case 'Int32EnumTypeAnnotation': + return `value == null ? ${typeAnnotation.default} : ((Double) value).intValue()`; + case 'MixedTypeAnnotation': + return 'new DynamicFromObject(value)'; + default: + typeAnnotation; + throw new Error('Received invalid typeAnnotation'); + } +} +function generatePropCasesString(component, componentName) { + if (component.props.length === 0) { + return 'super.setProperty(view, propName, value);'; + } + const cases = component.props + .map(prop => { + return `case "${prop.name}": + mViewManager.set${toSafeJavaString( + prop.name, + )}(view, ${getJavaValueForProp(prop, componentName)}); + break;`; + }) + .join('\n' + ' '); + return `switch (propName) { + ${cases} + default: + super.setProperty(view, propName, value); + }`; +} +function getCommandArgJavaType(param, index) { + const typeAnnotation = param.typeAnnotation; + switch (typeAnnotation.type) { + case 'ReservedTypeAnnotation': + switch (typeAnnotation.name) { + case 'RootTag': + return `args.getDouble(${index})`; + default: + typeAnnotation.name; + throw new Error(`Receieved invalid type: ${typeAnnotation.name}`); + } + case 'BooleanTypeAnnotation': + return `args.getBoolean(${index})`; + case 'DoubleTypeAnnotation': + return `args.getDouble(${index})`; + case 'FloatTypeAnnotation': + return `(float) args.getDouble(${index})`; + case 'Int32TypeAnnotation': + return `args.getInt(${index})`; + case 'StringTypeAnnotation': + return `args.getString(${index})`; + case 'ArrayTypeAnnotation': + return `args.getArray(${index})`; + default: + typeAnnotation.type; + throw new Error(`Receieved invalid type: ${typeAnnotation.type}`); + } +} +function getCommandArguments(command) { + return [ + 'view', + ...command.typeAnnotation.params.map(getCommandArgJavaType), + ].join(', '); +} +function generateCommandCasesString(component, componentName) { + if (component.commands.length === 0) { + return null; + } + const commandMethods = component.commands + .map(command => { + return `case "${command.name}": + mViewManager.${toSafeJavaString( + command.name, + false, + )}(${getCommandArguments(command)}); + break;`; + }) + .join('\n' + ' '); + return commandMethods; +} +function getClassExtendString(component) { + const extendString = component.extendsProps + .map(extendProps => { + switch (extendProps.type) { + case 'ReactNativeBuiltInType': + switch (extendProps.knownTypeName) { + case 'ReactNativeCoreViewProps': + return 'View'; + default: + extendProps.knownTypeName; + throw new Error('Invalid knownTypeName'); + } + default: + extendProps.type; + throw new Error('Invalid extended type'); + } + }) + .join(''); + return extendString; +} +function getDelegateImports(component) { + const imports = getImports(component, 'delegate'); + // The delegate needs ReadableArray for commands always. + // The interface doesn't always need it + if (component.commands.length > 0) { + imports.add('import com.facebook.react.bridge.ReadableArray;'); + } + imports.add('import androidx.annotation.Nullable;'); + imports.add('import com.facebook.react.uimanager.BaseViewManagerDelegate;'); + imports.add('import com.facebook.react.uimanager.BaseViewManagerInterface;'); + return imports; +} +function generateMethods(propsString, commandsString) { + return [ + PropSetterTemplate({ + propCases: propsString, + }), + commandsString != null + ? CommandsTemplate({ + commandCases: commandsString, + }) + : '', + ] + .join('\n\n ') + .trimRight(); +} +module.exports = { + generate( + libraryName, + schema, + packageName, + assumeNonnull = false, + headerPrefix, + ) { + // TODO: This doesn't support custom package name yet. + const normalizedPackageName = 'com.facebook.react.viewmanagers'; + const outputDir = `java/${normalizedPackageName.replace(/\./g, '/')}`; + const files = new Map(); + Object.keys(schema.modules).forEach(moduleName => { + const module = schema.modules[moduleName]; + if (module.type !== 'Component') { + return; + } + const components = module.components; + // No components in this module + if (components == null) { + return; + } + return Object.keys(components) + .filter(componentName => { + const component = components[componentName]; + return !( + component.excludedPlatforms && + component.excludedPlatforms.includes('android') + ); + }) + .forEach(componentName => { + const component = components[componentName]; + const className = getDelegateJavaClassName(componentName); + const interfaceClassName = getInterfaceJavaClassName(componentName); + const imports = getDelegateImports(component); + const propsString = generatePropCasesString(component, componentName); + const commandsString = generateCommandCasesString( + component, + componentName, + ); + const extendString = getClassExtendString(component); + const replacedTemplate = FileTemplate({ + imports: Array.from(imports).sort().join('\n'), + packageName: normalizedPackageName, + className, + extendClasses: extendString, + methods: generateMethods(propsString, commandsString), + interfaceClassName: interfaceClassName, + }); + files.set(`${outputDir}/${className}.java`, replacedTemplate); + }); + }); + return files; + }, +}; diff --git a/packages/react-native-codegen/lib/generators/components/GeneratePropsJavaDelegate.js.flow b/packages/react-native-codegen/lib/generators/components/GeneratePropsJavaDelegate.js.flow new file mode 100644 index 00000000000000..a758d69ecc3fb8 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/GeneratePropsJavaDelegate.js.flow @@ -0,0 +1,355 @@ +/** + * 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 {CommandParamTypeAnnotation} from '../../CodegenSchema'; +import type { + CommandTypeAnnotation, + ComponentShape, + NamedShape, + PropTypeAnnotation, + SchemaType, +} from '../../CodegenSchema'; + +const { + getDelegateJavaClassName, + getImports, + getInterfaceJavaClassName, + toSafeJavaString, +} = require('./JavaHelpers'); + +// File path -> contents +type FilesOutput = Map; + +const FileTemplate = ({ + packageName, + imports, + className, + extendClasses, + interfaceClassName, + methods, +}: { + packageName: string, + imports: string, + className: string, + extendClasses: string, + interfaceClassName: string, + methods: 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: GeneratePropsJavaDelegate.js +*/ + +package ${packageName}; + +${imports} + +public class ${className} & ${interfaceClassName}> extends BaseViewManagerDelegate { + public ${className}(U viewManager) { + super(viewManager); + } + ${methods} +} +`; + +const PropSetterTemplate = ({propCases}: {propCases: string}) => + ` + @Override + public void setProperty(T view, String propName, @Nullable Object value) { + ${propCases} + } +`.trim(); + +const CommandsTemplate = ({commandCases}: {commandCases: string}) => + ` + @Override + public void receiveCommand(T view, String commandName, @Nullable ReadableArray args) { + switch (commandName) { + ${commandCases} + } + } +`.trim(); + +function getJavaValueForProp( + prop: NamedShape, + componentName: string, +): string { + const typeAnnotation = prop.typeAnnotation; + + switch (typeAnnotation.type) { + case 'BooleanTypeAnnotation': + if (typeAnnotation.default === null) { + return 'value == null ? null : (Boolean) value'; + } else { + return `value == null ? ${typeAnnotation.default.toString()} : (boolean) value`; + } + case 'StringTypeAnnotation': + const defaultValueString = + typeAnnotation.default === null + ? 'null' + : `"${typeAnnotation.default}"`; + return `value == null ? ${defaultValueString} : (String) value`; + case 'Int32TypeAnnotation': + return `value == null ? ${typeAnnotation.default} : ((Double) value).intValue()`; + case 'DoubleTypeAnnotation': + if (prop.optional) { + return `value == null ? ${typeAnnotation.default}f : ((Double) value).doubleValue()`; + } else { + return 'value == null ? Double.NaN : ((Double) value).doubleValue()'; + } + case 'FloatTypeAnnotation': + if (typeAnnotation.default === null) { + return 'value == null ? null : ((Double) value).floatValue()'; + } else if (prop.optional) { + return `value == null ? ${typeAnnotation.default}f : ((Double) value).floatValue()`; + } else { + return 'value == null ? Float.NaN : ((Double) value).floatValue()'; + } + case 'ReservedPropTypeAnnotation': + switch (typeAnnotation.name) { + case 'ColorPrimitive': + return 'ColorPropConverter.getColor(value, view.getContext())'; + case 'ImageSourcePrimitive': + return '(ReadableMap) value'; + case 'ImageRequestPrimitive': + return '(ReadableMap) value'; + case 'PointPrimitive': + return '(ReadableMap) value'; + case 'EdgeInsetsPrimitive': + return '(ReadableMap) value'; + case 'DimensionPrimitive': + return 'DimensionPropConverter.getDimension(value)'; + default: + (typeAnnotation.name: empty); + throw new Error('Received unknown ReservedPropTypeAnnotation'); + } + case 'ArrayTypeAnnotation': { + return '(ReadableArray) value'; + } + case 'ObjectTypeAnnotation': { + return '(ReadableMap) value'; + } + case 'StringEnumTypeAnnotation': + return '(String) value'; + case 'Int32EnumTypeAnnotation': + return `value == null ? ${typeAnnotation.default} : ((Double) value).intValue()`; + case 'MixedTypeAnnotation': + return 'new DynamicFromObject(value)'; + default: + (typeAnnotation: empty); + throw new Error('Received invalid typeAnnotation'); + } +} + +function generatePropCasesString( + component: ComponentShape, + componentName: string, +) { + if (component.props.length === 0) { + return 'super.setProperty(view, propName, value);'; + } + + const cases = component.props + .map(prop => { + return `case "${prop.name}": + mViewManager.set${toSafeJavaString( + prop.name, + )}(view, ${getJavaValueForProp(prop, componentName)}); + break;`; + }) + .join('\n' + ' '); + + return `switch (propName) { + ${cases} + default: + super.setProperty(view, propName, value); + }`; +} + +function getCommandArgJavaType( + param: NamedShape, + index: number, +) { + const {typeAnnotation} = param; + + switch (typeAnnotation.type) { + case 'ReservedTypeAnnotation': + switch (typeAnnotation.name) { + case 'RootTag': + return `args.getDouble(${index})`; + default: + (typeAnnotation.name: empty); + throw new Error(`Receieved invalid type: ${typeAnnotation.name}`); + } + case 'BooleanTypeAnnotation': + return `args.getBoolean(${index})`; + case 'DoubleTypeAnnotation': + return `args.getDouble(${index})`; + case 'FloatTypeAnnotation': + return `(float) args.getDouble(${index})`; + case 'Int32TypeAnnotation': + return `args.getInt(${index})`; + case 'StringTypeAnnotation': + return `args.getString(${index})`; + case 'ArrayTypeAnnotation': + return `args.getArray(${index})`; + default: + (typeAnnotation.type: empty); + throw new Error(`Receieved invalid type: ${typeAnnotation.type}`); + } +} + +function getCommandArguments( + command: NamedShape, +): string { + return [ + 'view', + ...command.typeAnnotation.params.map(getCommandArgJavaType), + ].join(', '); +} + +function generateCommandCasesString( + component: ComponentShape, + componentName: string, +) { + if (component.commands.length === 0) { + return null; + } + + const commandMethods = component.commands + .map(command => { + return `case "${command.name}": + mViewManager.${toSafeJavaString( + command.name, + false, + )}(${getCommandArguments(command)}); + break;`; + }) + .join('\n' + ' '); + + return commandMethods; +} + +function getClassExtendString(component: ComponentShape): string { + const extendString = component.extendsProps + .map(extendProps => { + switch (extendProps.type) { + case 'ReactNativeBuiltInType': + switch (extendProps.knownTypeName) { + case 'ReactNativeCoreViewProps': + return 'View'; + default: + (extendProps.knownTypeName: empty); + throw new Error('Invalid knownTypeName'); + } + default: + (extendProps.type: empty); + throw new Error('Invalid extended type'); + } + }) + .join(''); + + return extendString; +} + +function getDelegateImports(component: ComponentShape) { + const imports = getImports(component, 'delegate'); + // The delegate needs ReadableArray for commands always. + // The interface doesn't always need it + if (component.commands.length > 0) { + imports.add('import com.facebook.react.bridge.ReadableArray;'); + } + imports.add('import androidx.annotation.Nullable;'); + imports.add('import com.facebook.react.uimanager.BaseViewManagerDelegate;'); + imports.add('import com.facebook.react.uimanager.BaseViewManagerInterface;'); + + return imports; +} + +function generateMethods( + propsString: string, + commandsString: null | string, +): string { + return [ + PropSetterTemplate({propCases: propsString}), + commandsString != null + ? CommandsTemplate({commandCases: commandsString}) + : '', + ] + .join('\n\n ') + .trimRight(); +} + +module.exports = { + generate( + libraryName: string, + schema: SchemaType, + packageName?: string, + assumeNonnull: boolean = false, + headerPrefix?: string, + ): FilesOutput { + // TODO: This doesn't support custom package name yet. + const normalizedPackageName = 'com.facebook.react.viewmanagers'; + const outputDir = `java/${normalizedPackageName.replace(/\./g, '/')}`; + + const files = new Map(); + Object.keys(schema.modules).forEach(moduleName => { + const module = schema.modules[moduleName]; + if (module.type !== 'Component') { + return; + } + + const {components} = module; + // No components in this module + if (components == null) { + return; + } + + return Object.keys(components) + .filter(componentName => { + const component = components[componentName]; + return !( + component.excludedPlatforms && + component.excludedPlatforms.includes('android') + ); + }) + .forEach(componentName => { + const component = components[componentName]; + const className = getDelegateJavaClassName(componentName); + const interfaceClassName = getInterfaceJavaClassName(componentName); + + const imports = getDelegateImports(component); + const propsString = generatePropCasesString(component, componentName); + const commandsString = generateCommandCasesString( + component, + componentName, + ); + const extendString = getClassExtendString(component); + + const replacedTemplate = FileTemplate({ + imports: Array.from(imports).sort().join('\n'), + packageName: normalizedPackageName, + className, + extendClasses: extendString, + methods: generateMethods(propsString, commandsString), + interfaceClassName: interfaceClassName, + }); + + files.set(`${outputDir}/${className}.java`, replacedTemplate); + }); + }); + + return files; + }, +}; diff --git a/packages/react-native-codegen/lib/generators/components/GeneratePropsJavaInterface.js b/packages/react-native-codegen/lib/generators/components/GeneratePropsJavaInterface.js new file mode 100644 index 00000000000000..3779a355a9839d --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/GeneratePropsJavaInterface.js @@ -0,0 +1,250 @@ +/** + * 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 _require = require('./JavaHelpers'), + getImports = _require.getImports, + getInterfaceJavaClassName = _require.getInterfaceJavaClassName, + toSafeJavaString = _require.toSafeJavaString; + +// File path -> contents + +const FileTemplate = ({ + packageName, + imports, + className, + extendClasses, + methods, +}) => `/** +* 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: GeneratePropsJavaInterface.js +*/ + +package ${packageName}; + +${imports} + +public interface ${className} { + ${methods} +} +`; +function addNullable(imports) { + imports.add('import androidx.annotation.Nullable;'); +} +function getJavaValueForProp(prop, imports) { + const typeAnnotation = prop.typeAnnotation; + switch (typeAnnotation.type) { + case 'BooleanTypeAnnotation': + if (typeAnnotation.default === null) { + addNullable(imports); + return '@Nullable Boolean value'; + } else { + return 'boolean value'; + } + case 'StringTypeAnnotation': + addNullable(imports); + return '@Nullable String value'; + case 'Int32TypeAnnotation': + return 'int value'; + case 'DoubleTypeAnnotation': + return 'double value'; + case 'FloatTypeAnnotation': + if (typeAnnotation.default === null) { + addNullable(imports); + return '@Nullable Float value'; + } else { + return 'float value'; + } + case 'ReservedPropTypeAnnotation': + switch (typeAnnotation.name) { + case 'ColorPrimitive': + addNullable(imports); + return '@Nullable Integer value'; + case 'ImageSourcePrimitive': + addNullable(imports); + return '@Nullable ReadableMap value'; + case 'ImageRequestPrimitive': + addNullable(imports); + return '@Nullable ReadableMap value'; + case 'PointPrimitive': + addNullable(imports); + return '@Nullable ReadableMap value'; + case 'EdgeInsetsPrimitive': + addNullable(imports); + return '@Nullable ReadableMap value'; + case 'DimensionPrimitive': + addNullable(imports); + return '@Nullable YogaValue value'; + default: + typeAnnotation.name; + throw new Error('Received unknown ReservedPropTypeAnnotation'); + } + case 'ArrayTypeAnnotation': { + addNullable(imports); + return '@Nullable ReadableArray value'; + } + case 'ObjectTypeAnnotation': { + addNullable(imports); + return '@Nullable ReadableMap value'; + } + case 'StringEnumTypeAnnotation': + addNullable(imports); + return '@Nullable String value'; + case 'Int32EnumTypeAnnotation': + addNullable(imports); + return '@Nullable Integer value'; + case 'MixedTypeAnnotation': + return 'Dynamic value'; + default: + typeAnnotation; + throw new Error('Received invalid typeAnnotation'); + } +} +function generatePropsString(component, imports) { + if (component.props.length === 0) { + return '// No props'; + } + return component.props + .map(prop => { + return `void set${toSafeJavaString( + prop.name, + )}(T view, ${getJavaValueForProp(prop, imports)});`; + }) + .join('\n' + ' '); +} +function getCommandArgJavaType(param) { + const typeAnnotation = param.typeAnnotation; + switch (typeAnnotation.type) { + case 'ReservedTypeAnnotation': + switch (typeAnnotation.name) { + case 'RootTag': + return 'double'; + default: + typeAnnotation.name; + throw new Error(`Receieved invalid type: ${typeAnnotation.name}`); + } + case 'BooleanTypeAnnotation': + return 'boolean'; + case 'DoubleTypeAnnotation': + return 'double'; + case 'FloatTypeAnnotation': + return 'float'; + case 'Int32TypeAnnotation': + return 'int'; + case 'StringTypeAnnotation': + return 'String'; + case 'ArrayTypeAnnotation': + return 'ReadableArray'; + default: + typeAnnotation.type; + throw new Error('Receieved invalid typeAnnotation'); + } +} +function getCommandArguments(command, componentName) { + return [ + 'T view', + ...command.typeAnnotation.params.map(param => { + const commandArgJavaType = getCommandArgJavaType(param); + return `${commandArgJavaType} ${param.name}`; + }), + ].join(', '); +} +function generateCommandsString(component, componentName) { + return component.commands + .map(command => { + const safeJavaName = toSafeJavaString(command.name, false); + return `void ${safeJavaName}(${getCommandArguments( + command, + componentName, + )});`; + }) + .join('\n' + ' '); +} +function getClassExtendString(component) { + const extendString = component.extendsProps + .map(extendProps => { + switch (extendProps.type) { + case 'ReactNativeBuiltInType': + switch (extendProps.knownTypeName) { + case 'ReactNativeCoreViewProps': + return 'View'; + default: + extendProps.knownTypeName; + throw new Error('Invalid knownTypeName'); + } + default: + extendProps.type; + throw new Error('Invalid extended type'); + } + }) + .join(''); + return extendString; +} +module.exports = { + generate( + libraryName, + schema, + packageName, + assumeNonnull = false, + headerPrefix, + ) { + // TODO: This doesn't support custom package name yet. + const normalizedPackageName = 'com.facebook.react.viewmanagers'; + const outputDir = `java/${normalizedPackageName.replace(/\./g, '/')}`; + const files = new Map(); + Object.keys(schema.modules).forEach(moduleName => { + const module = schema.modules[moduleName]; + if (module.type !== 'Component') { + return; + } + const components = module.components; + + // No components in this module + if (components == null) { + return; + } + return Object.keys(components) + .filter(componentName => { + const component = components[componentName]; + return !( + component.excludedPlatforms && + component.excludedPlatforms.includes('android') + ); + }) + .forEach(componentName => { + const component = components[componentName]; + const className = getInterfaceJavaClassName(componentName); + const imports = getImports(component, 'interface'); + const propsString = generatePropsString(component, imports); + const commandsString = generateCommandsString( + component, + componentName, + ); + const extendString = getClassExtendString(component); + const replacedTemplate = FileTemplate({ + imports: Array.from(imports).sort().join('\n'), + packageName: normalizedPackageName, + className, + extendClasses: extendString, + methods: [propsString, commandsString] + .join('\n' + ' ') + .trimRight(), + }); + files.set(`${outputDir}/${className}.java`, replacedTemplate); + }); + }); + return files; + }, +}; diff --git a/packages/react-native-codegen/lib/generators/components/GeneratePropsJavaInterface.js.flow b/packages/react-native-codegen/lib/generators/components/GeneratePropsJavaInterface.js.flow new file mode 100644 index 00000000000000..28888b203e245b --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/GeneratePropsJavaInterface.js.flow @@ -0,0 +1,296 @@ +/** + * 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 {CommandParamTypeAnnotation} from '../../CodegenSchema'; +import type { + CommandTypeAnnotation, + ComponentShape, + NamedShape, + PropTypeAnnotation, + SchemaType, +} from '../../CodegenSchema'; + +const { + getImports, + getInterfaceJavaClassName, + toSafeJavaString, +} = require('./JavaHelpers'); + +// File path -> contents +type FilesOutput = Map; + +const FileTemplate = ({ + packageName, + imports, + className, + extendClasses, + methods, +}: { + packageName: string, + imports: string, + className: string, + extendClasses: string, + methods: 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: GeneratePropsJavaInterface.js +*/ + +package ${packageName}; + +${imports} + +public interface ${className} { + ${methods} +} +`; + +function addNullable(imports: Set) { + imports.add('import androidx.annotation.Nullable;'); +} + +function getJavaValueForProp( + prop: NamedShape, + imports: Set, +): string { + const typeAnnotation = prop.typeAnnotation; + + switch (typeAnnotation.type) { + case 'BooleanTypeAnnotation': + if (typeAnnotation.default === null) { + addNullable(imports); + return '@Nullable Boolean value'; + } else { + return 'boolean value'; + } + case 'StringTypeAnnotation': + addNullable(imports); + return '@Nullable String value'; + case 'Int32TypeAnnotation': + return 'int value'; + case 'DoubleTypeAnnotation': + return 'double value'; + case 'FloatTypeAnnotation': + if (typeAnnotation.default === null) { + addNullable(imports); + return '@Nullable Float value'; + } else { + return 'float value'; + } + case 'ReservedPropTypeAnnotation': + switch (typeAnnotation.name) { + case 'ColorPrimitive': + addNullable(imports); + return '@Nullable Integer value'; + case 'ImageSourcePrimitive': + addNullable(imports); + return '@Nullable ReadableMap value'; + case 'ImageRequestPrimitive': + addNullable(imports); + return '@Nullable ReadableMap value'; + case 'PointPrimitive': + addNullable(imports); + return '@Nullable ReadableMap value'; + case 'EdgeInsetsPrimitive': + addNullable(imports); + return '@Nullable ReadableMap value'; + case 'DimensionPrimitive': + addNullable(imports); + return '@Nullable YogaValue value'; + default: + (typeAnnotation.name: empty); + throw new Error('Received unknown ReservedPropTypeAnnotation'); + } + case 'ArrayTypeAnnotation': { + addNullable(imports); + return '@Nullable ReadableArray value'; + } + case 'ObjectTypeAnnotation': { + addNullable(imports); + return '@Nullable ReadableMap value'; + } + case 'StringEnumTypeAnnotation': + addNullable(imports); + return '@Nullable String value'; + case 'Int32EnumTypeAnnotation': + addNullable(imports); + return '@Nullable Integer value'; + case 'MixedTypeAnnotation': + return 'Dynamic value'; + default: + (typeAnnotation: empty); + throw new Error('Received invalid typeAnnotation'); + } +} + +function generatePropsString(component: ComponentShape, imports: Set) { + if (component.props.length === 0) { + return '// No props'; + } + + return component.props + .map(prop => { + return `void set${toSafeJavaString( + prop.name, + )}(T view, ${getJavaValueForProp(prop, imports)});`; + }) + .join('\n' + ' '); +} + +function getCommandArgJavaType(param: NamedShape) { + const {typeAnnotation} = param; + + switch (typeAnnotation.type) { + case 'ReservedTypeAnnotation': + switch (typeAnnotation.name) { + case 'RootTag': + return 'double'; + default: + (typeAnnotation.name: empty); + throw new Error(`Receieved invalid type: ${typeAnnotation.name}`); + } + case 'BooleanTypeAnnotation': + return 'boolean'; + case 'DoubleTypeAnnotation': + return 'double'; + case 'FloatTypeAnnotation': + return 'float'; + case 'Int32TypeAnnotation': + return 'int'; + case 'StringTypeAnnotation': + return 'String'; + case 'ArrayTypeAnnotation': + return 'ReadableArray'; + default: + (typeAnnotation.type: empty); + throw new Error('Receieved invalid typeAnnotation'); + } +} + +function getCommandArguments( + command: NamedShape, + componentName: string, +): string { + return [ + 'T view', + ...command.typeAnnotation.params.map(param => { + const commandArgJavaType = getCommandArgJavaType(param); + + return `${commandArgJavaType} ${param.name}`; + }), + ].join(', '); +} + +function generateCommandsString( + component: ComponentShape, + componentName: string, +) { + return component.commands + .map(command => { + const safeJavaName = toSafeJavaString(command.name, false); + + return `void ${safeJavaName}(${getCommandArguments( + command, + componentName, + )});`; + }) + .join('\n' + ' '); +} + +function getClassExtendString(component: ComponentShape): string { + const extendString = component.extendsProps + .map(extendProps => { + switch (extendProps.type) { + case 'ReactNativeBuiltInType': + switch (extendProps.knownTypeName) { + case 'ReactNativeCoreViewProps': + return 'View'; + default: + (extendProps.knownTypeName: empty); + throw new Error('Invalid knownTypeName'); + } + default: + (extendProps.type: empty); + throw new Error('Invalid extended type'); + } + }) + .join(''); + + return extendString; +} + +module.exports = { + generate( + libraryName: string, + schema: SchemaType, + packageName?: string, + assumeNonnull: boolean = false, + headerPrefix?: string, + ): FilesOutput { + // TODO: This doesn't support custom package name yet. + const normalizedPackageName = 'com.facebook.react.viewmanagers'; + const outputDir = `java/${normalizedPackageName.replace(/\./g, '/')}`; + + const files = new Map(); + Object.keys(schema.modules).forEach(moduleName => { + const module = schema.modules[moduleName]; + if (module.type !== 'Component') { + return; + } + + const {components} = module; + + // No components in this module + if (components == null) { + return; + } + + return Object.keys(components) + .filter(componentName => { + const component = components[componentName]; + return !( + component.excludedPlatforms && + component.excludedPlatforms.includes('android') + ); + }) + .forEach(componentName => { + const component = components[componentName]; + const className = getInterfaceJavaClassName(componentName); + + const imports = getImports(component, 'interface'); + const propsString = generatePropsString(component, imports); + const commandsString = generateCommandsString( + component, + componentName, + ); + const extendString = getClassExtendString(component); + + const replacedTemplate = FileTemplate({ + imports: Array.from(imports).sort().join('\n'), + packageName: normalizedPackageName, + className, + extendClasses: extendString, + methods: [propsString, commandsString] + .join('\n' + ' ') + .trimRight(), + }); + + files.set(`${outputDir}/${className}.java`, replacedTemplate); + }); + }); + + return files; + }, +}; diff --git a/packages/react-native-codegen/lib/generators/components/GeneratePropsJavaPojo/PojoCollector.js b/packages/react-native-codegen/lib/generators/components/GeneratePropsJavaPojo/PojoCollector.js new file mode 100644 index 00000000000000..bb57fba7ef5df7 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/GeneratePropsJavaPojo/PojoCollector.js @@ -0,0 +1,148 @@ +/** + * 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'; + +function ownKeys(e, r) { + var t = Object.keys(e); + if (Object.getOwnPropertySymbols) { + var o = Object.getOwnPropertySymbols(e); + r && + (o = o.filter(function (r) { + return Object.getOwnPropertyDescriptor(e, r).enumerable; + })), + t.push.apply(t, o); + } + return t; +} +function _objectSpread(e) { + for (var r = 1; r < arguments.length; r++) { + var t = null != arguments[r] ? arguments[r] : {}; + r % 2 + ? ownKeys(Object(t), !0).forEach(function (r) { + _defineProperty(e, r, t[r]); + }) + : Object.getOwnPropertyDescriptors + ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) + : ownKeys(Object(t)).forEach(function (r) { + Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); + }); + } + return e; +} +function _defineProperty(e, r, t) { + return ( + (r = _toPropertyKey(r)) in e + ? Object.defineProperty(e, r, { + value: t, + enumerable: !0, + configurable: !0, + writable: !0, + }) + : (e[r] = t), + e + ); +} +function _toPropertyKey(t) { + var i = _toPrimitive(t, 'string'); + return 'symbol' == typeof i ? i : i + ''; +} +function _toPrimitive(t, r) { + if ('object' != typeof t || !t) return t; + var e = t[Symbol.toPrimitive]; + if (void 0 !== e) { + var i = e.call(t, r || 'default'); + if ('object' != typeof i) return i; + throw new TypeError('@@toPrimitive must return a primitive value.'); + } + return ('string' === r ? String : Number)(t); +} +const _require = require('../../Utils'), + capitalize = _require.capitalize; +class PojoCollector { + constructor() { + _defineProperty(this, '_pojos', new Map()); + } + process(namespace, pojoName, typeAnnotation) { + switch (typeAnnotation.type) { + case 'ObjectTypeAnnotation': { + this._insertPojo(namespace, pojoName, typeAnnotation); + return { + type: 'PojoTypeAliasTypeAnnotation', + name: pojoName, + }; + } + case 'ArrayTypeAnnotation': { + const arrayTypeAnnotation = typeAnnotation; + const elementType = arrayTypeAnnotation.elementType; + const pojoElementType = (() => { + switch (elementType.type) { + case 'ObjectTypeAnnotation': { + this._insertPojo(namespace, `${pojoName}Element`, elementType); + return { + type: 'PojoTypeAliasTypeAnnotation', + name: `${pojoName}Element`, + }; + } + case 'ArrayTypeAnnotation': { + const objectTypeAnnotation = elementType.elementType; + this._insertPojo( + namespace, + `${pojoName}ElementElement`, + objectTypeAnnotation, + ); + return { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'PojoTypeAliasTypeAnnotation', + name: `${pojoName}ElementElement`, + }, + }; + } + default: { + return elementType; + } + } + })(); + return { + type: 'ArrayTypeAnnotation', + elementType: pojoElementType, + }; + } + default: + return typeAnnotation; + } + } + _insertPojo(namespace, pojoName, objectTypeAnnotation) { + const properties = objectTypeAnnotation.properties.map(property => { + const propertyPojoName = pojoName + capitalize(property.name); + return _objectSpread( + _objectSpread({}, property), + {}, + { + typeAnnotation: this.process( + namespace, + propertyPojoName, + property.typeAnnotation, + ), + }, + ); + }); + this._pojos.set(pojoName, { + name: pojoName, + namespace, + properties, + }); + } + getAllPojos() { + return [...this._pojos.values()]; + } +} +module.exports = PojoCollector; diff --git a/packages/react-native-codegen/lib/generators/components/GeneratePropsJavaPojo/PojoCollector.js.flow b/packages/react-native-codegen/lib/generators/components/GeneratePropsJavaPojo/PojoCollector.js.flow new file mode 100644 index 00000000000000..6f5e19e88fd243 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/GeneratePropsJavaPojo/PojoCollector.js.flow @@ -0,0 +1,187 @@ +/** + * 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 { + ArrayTypeAnnotation, + BooleanTypeAnnotation, + DoubleTypeAnnotation, + FloatTypeAnnotation, + Int32TypeAnnotation, + MixedTypeAnnotation, + NamedShape, + ObjectTypeAnnotation, + PropTypeAnnotation, + ReservedPropTypeAnnotation, + StringTypeAnnotation, +} from '../../../CodegenSchema'; + +const {capitalize} = require('../../Utils'); + +export type Pojo = { + name: string, + namespace: string, + properties: $ReadOnlyArray, +}; + +export type PojoProperty = NamedShape; + +export type PojoTypeAliasAnnotation = { + type: 'PojoTypeAliasTypeAnnotation', + name: string, +}; + +export type PojoTypeAnnotation = + | $ReadOnly<{ + type: 'BooleanTypeAnnotation', + default: boolean | null, + }> + | $ReadOnly<{ + type: 'StringTypeAnnotation', + default: string | null, + }> + | $ReadOnly<{ + type: 'DoubleTypeAnnotation', + default: number, + }> + | $ReadOnly<{ + type: 'FloatTypeAnnotation', + default: number | null, + }> + | $ReadOnly<{ + type: 'Int32TypeAnnotation', + default: number, + }> + | $ReadOnly<{ + type: 'StringEnumTypeAnnotation', + default: string, + options: $ReadOnlyArray, + }> + | $ReadOnly<{ + type: 'Int32EnumTypeAnnotation', + default: number, + options: $ReadOnlyArray, + }> + | ReservedPropTypeAnnotation + | PojoTypeAliasAnnotation + | $ReadOnly<{ + type: 'ArrayTypeAnnotation', + elementType: + | BooleanTypeAnnotation + | StringTypeAnnotation + | DoubleTypeAnnotation + | FloatTypeAnnotation + | Int32TypeAnnotation + | $ReadOnly<{ + type: 'StringEnumTypeAnnotation', + default: string, + options: $ReadOnlyArray, + }> + | PojoTypeAliasAnnotation + | ReservedPropTypeAnnotation + | $ReadOnly<{ + type: 'ArrayTypeAnnotation', + elementType: PojoTypeAliasAnnotation, + }>, + }> + | MixedTypeAnnotation; + +class PojoCollector { + _pojos: Map = new Map(); + process( + namespace: string, + pojoName: string, + typeAnnotation: PropTypeAnnotation, + ): PojoTypeAnnotation { + switch (typeAnnotation.type) { + case 'ObjectTypeAnnotation': { + this._insertPojo(namespace, pojoName, typeAnnotation); + return { + type: 'PojoTypeAliasTypeAnnotation', + name: pojoName, + }; + } + case 'ArrayTypeAnnotation': { + const arrayTypeAnnotation = typeAnnotation; + const elementType: $PropertyType = + arrayTypeAnnotation.elementType; + + const pojoElementType = (() => { + switch (elementType.type) { + case 'ObjectTypeAnnotation': { + this._insertPojo(namespace, `${pojoName}Element`, elementType); + return { + type: 'PojoTypeAliasTypeAnnotation', + name: `${pojoName}Element`, + }; + } + case 'ArrayTypeAnnotation': { + const {elementType: objectTypeAnnotation} = elementType; + this._insertPojo( + namespace, + `${pojoName}ElementElement`, + objectTypeAnnotation, + ); + return { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'PojoTypeAliasTypeAnnotation', + name: `${pojoName}ElementElement`, + }, + }; + } + default: { + return elementType; + } + } + })(); + + return { + type: 'ArrayTypeAnnotation', + elementType: pojoElementType, + }; + } + default: + return typeAnnotation; + } + } + + _insertPojo( + namespace: string, + pojoName: string, + objectTypeAnnotation: ObjectTypeAnnotation, + ) { + const properties = objectTypeAnnotation.properties.map(property => { + const propertyPojoName = pojoName + capitalize(property.name); + + return { + ...property, + typeAnnotation: this.process( + namespace, + propertyPojoName, + property.typeAnnotation, + ), + }; + }); + + this._pojos.set(pojoName, { + name: pojoName, + namespace, + properties, + }); + } + + getAllPojos(): $ReadOnlyArray { + return [...this._pojos.values()]; + } +} + +module.exports = PojoCollector; diff --git a/packages/react-native-codegen/lib/generators/components/GeneratePropsJavaPojo/index.js b/packages/react-native-codegen/lib/generators/components/GeneratePropsJavaPojo/index.js new file mode 100644 index 00000000000000..7ecd1bcf0f84a2 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/GeneratePropsJavaPojo/index.js @@ -0,0 +1,66 @@ +/** + * 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 _require = require('../../Utils'), + capitalize = _require.capitalize; +const PojoCollector = require('./PojoCollector'); +const _require2 = require('./serializePojo'), + serializePojo = _require2.serializePojo; +module.exports = { + generate(libraryName, schema, packageName) { + const pojoCollector = new PojoCollector(); + const basePackageName = 'com.facebook.react.viewmanagers'; + Object.keys(schema.modules).forEach(hasteModuleName => { + const module = schema.modules[hasteModuleName]; + if (module.type !== 'Component') { + return; + } + const components = module.components; + // No components in this module + if (components == null) { + return null; + } + Object.keys(components) + .filter(componentName => { + const component = components[componentName]; + return !( + component.excludedPlatforms && + component.excludedPlatforms.includes('android') + ); + }) + .forEach(componentName => { + const component = components[componentName]; + if (component == null) { + return; + } + const props = component.props; + pojoCollector.process( + capitalize(hasteModuleName), + `${capitalize(componentName)}Props`, + { + type: 'ObjectTypeAnnotation', + properties: props, + }, + ); + }); + }); + const pojoDir = basePackageName.split('.').join('/'); + return new Map( + pojoCollector.getAllPojos().map(pojo => { + return [ + `java/${pojoDir}/${pojo.namespace}/${pojo.name}.java`, + serializePojo(pojo, basePackageName), + ]; + }), + ); + }, +}; diff --git a/packages/react-native-codegen/lib/generators/components/GeneratePropsJavaPojo/index.js.flow b/packages/react-native-codegen/lib/generators/components/GeneratePropsJavaPojo/index.js.flow new file mode 100644 index 00000000000000..a60583debc5474 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/GeneratePropsJavaPojo/index.js.flow @@ -0,0 +1,80 @@ +/** + * 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 {SchemaType} from '../../../CodegenSchema'; + +const {capitalize} = require('../../Utils'); +const PojoCollector = require('./PojoCollector'); +const {serializePojo} = require('./serializePojo'); + +type FilesOutput = Map; + +module.exports = { + generate( + libraryName: string, + schema: SchemaType, + packageName?: string, + ): FilesOutput { + const pojoCollector = new PojoCollector(); + const basePackageName = 'com.facebook.react.viewmanagers'; + + Object.keys(schema.modules).forEach(hasteModuleName => { + const module = schema.modules[hasteModuleName]; + if (module.type !== 'Component') { + return; + } + + const {components} = module; + // No components in this module + if (components == null) { + return null; + } + + Object.keys(components) + .filter(componentName => { + const component = components[componentName]; + return !( + component.excludedPlatforms && + component.excludedPlatforms.includes('android') + ); + }) + .forEach(componentName => { + const component = components[componentName]; + if (component == null) { + return; + } + + const {props} = component; + + pojoCollector.process( + capitalize(hasteModuleName), + `${capitalize(componentName)}Props`, + { + type: 'ObjectTypeAnnotation', + properties: props, + }, + ); + }); + }); + + const pojoDir = basePackageName.split('.').join('/'); + + return new Map( + pojoCollector.getAllPojos().map(pojo => { + return [ + `java/${pojoDir}/${pojo.namespace}/${pojo.name}.java`, + serializePojo(pojo, basePackageName), + ]; + }), + ); + }, +}; diff --git a/packages/react-native-codegen/lib/generators/components/GeneratePropsJavaPojo/serializePojo.js b/packages/react-native-codegen/lib/generators/components/GeneratePropsJavaPojo/serializePojo.js new file mode 100644 index 00000000000000..8e7e5a95a9d16b --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/GeneratePropsJavaPojo/serializePojo.js @@ -0,0 +1,286 @@ +/** + * 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 _require = require('../../Utils'), + capitalize = _require.capitalize; +function toJavaType(typeAnnotation, addImport) { + const importNullable = () => addImport('androidx.annotation.Nullable'); + const importReadableMap = () => + addImport('com.facebook.react.bridge.ReadableMap'); + const importArrayList = () => addImport('java.util.ArrayList'); + const importYogaValue = () => addImport('com.facebook.yoga.YogaValue'); + const importDynamic = () => addImport('com.facebook.react.bridge.Dynamic'); + switch (typeAnnotation.type) { + /** + * Primitives + */ + case 'BooleanTypeAnnotation': { + if (typeAnnotation.default === null) { + importNullable(); + return '@Nullable Boolean'; + } else { + return 'boolean'; + } + } + case 'StringTypeAnnotation': { + importNullable(); + return '@Nullable String'; + } + case 'DoubleTypeAnnotation': { + return 'double'; + } + case 'FloatTypeAnnotation': { + if (typeAnnotation.default === null) { + importNullable(); + return '@Nullable Float'; + } else { + return 'float'; + } + } + case 'Int32TypeAnnotation': { + return 'int'; + } + + /** + * Enums + */ + // TODO: Make StringEnumTypeAnnotation type-safe? + case 'StringEnumTypeAnnotation': + importNullable(); + return '@Nullable String'; + // TODO: Make Int32EnumTypeAnnotation type-safe? + case 'Int32EnumTypeAnnotation': + importNullable(); + return '@Nullable Integer'; + + /** + * Reserved types + */ + case 'ReservedPropTypeAnnotation': { + switch (typeAnnotation.name) { + case 'ColorPrimitive': + importNullable(); + return '@Nullable Integer'; + + // TODO: Make ImageSourcePrimitive type-safe + case 'ImageSourcePrimitive': + importNullable(); + importReadableMap(); + return '@Nullable ReadableMap'; + + // TODO: Make ImageRequestPrimitive type-safe + case 'ImageRequestPrimitive': + importNullable(); + importReadableMap(); + return '@Nullable ReadableMap'; + + // TODO: Make PointPrimitive type-safe + case 'PointPrimitive': + importNullable(); + importReadableMap(); + return '@Nullable ReadableMap'; + + // TODO: Make EdgeInsetsPrimitive type-safe + case 'EdgeInsetsPrimitive': + importNullable(); + importReadableMap(); + return '@Nullable ReadableMap'; + case 'DimensionPrimitive': + importNullable(); + importYogaValue(); + return '@Nullable YogaValue'; + default: + typeAnnotation.name; + throw new Error( + `Received unknown ReservedPropTypeAnnotation ${typeAnnotation.name}`, + ); + } + } + + /** + * Other Pojo objects + */ + case 'PojoTypeAliasTypeAnnotation': { + return typeAnnotation.name; + } + + /** + * Arrays + */ + case 'ArrayTypeAnnotation': { + const elementType = typeAnnotation.elementType; + const elementTypeString = (() => { + switch (elementType.type) { + /** + * Primitives + */ + case 'BooleanTypeAnnotation': { + return 'Boolean'; + } + case 'StringTypeAnnotation': { + return 'String'; + } + case 'DoubleTypeAnnotation': { + return 'Double'; + } + case 'FloatTypeAnnotation': { + return 'Float'; + } + case 'Int32TypeAnnotation': { + return 'Integer'; + } + + /** + * Enums + */ + // TODO: Make StringEnums type-safe in Pojos + case 'StringEnumTypeAnnotation': { + return 'String'; + } + + /** + * Other Pojo objects + */ + case 'PojoTypeAliasTypeAnnotation': { + return elementType.name; + } + + /** + * Reserved types + */ + case 'ReservedPropTypeAnnotation': { + switch (elementType.name) { + case 'ColorPrimitive': + return 'Integer'; + + // TODO: Make ImageSourcePrimitive type-safe + case 'ImageSourcePrimitive': + importReadableMap(); + return 'ReadableMap'; + + // TODO: Make ImageRequestPrimitive type-safe + case 'ImageRequestPrimitive': + importReadableMap(); + return 'ReadableMap'; + + // TODO: Make PointPrimitive type-safe + case 'PointPrimitive': + importReadableMap(); + return 'ReadableMap'; + + // TODO: Make EdgeInsetsPrimitive type-safe + case 'EdgeInsetsPrimitive': + importReadableMap(); + return 'ReadableMap'; + case 'DimensionPrimitive': + importYogaValue(); + return 'YogaValue'; + default: + elementType.name; + throw new Error( + `Received unknown ReservedPropTypeAnnotation ${elementType.name}`, + ); + } + } + + // Arrays + case 'ArrayTypeAnnotation': { + const pojoTypeAliasTypeAnnotation = elementType.elementType; + importArrayList(); + return `ArrayList<${pojoTypeAliasTypeAnnotation.name}>`; + } + default: { + elementType.type; + throw new Error( + `Unrecognized PojoTypeAnnotation Array element type annotation '${typeAnnotation.type}'`, + ); + } + } + })(); + importArrayList(); + return `ArrayList<${elementTypeString}>`; + } + case 'MixedTypeAnnotation': { + importDynamic(); + return 'Dynamic'; + } + default: { + typeAnnotation.type; + throw new Error( + `Unrecognized PojoTypeAnnotation '${typeAnnotation.type}'`, + ); + } + } +} +function toJavaMemberName(property) { + return `m${capitalize(property.name)}`; +} +function toJavaMemberDeclaration(property, addImport) { + const type = toJavaType(property.typeAnnotation, addImport); + const memberName = toJavaMemberName(property); + return `private ${type} ${memberName};`; +} +function toJavaGetter(property, addImport) { + const type = toJavaType(property.typeAnnotation, addImport); + const getterName = `get${capitalize(property.name)}`; + const memberName = toJavaMemberName(property); + addImport('com.facebook.proguard.annotations.DoNotStrip'); + return `@DoNotStrip +public ${type} ${getterName}() { + return ${memberName}; +}`; +} +function serializePojo(pojo, basePackageName) { + const importSet = new Set(); + const addImport = $import => { + importSet.add($import); + }; + addImport('com.facebook.proguard.annotations.DoNotStrip'); + const indent = ' '.repeat(2); + const members = pojo.properties + .map(property => toJavaMemberDeclaration(property, addImport)) + .map(member => `${indent}${member}`) + .join('\n'); + const getters = pojo.properties + .map(property => toJavaGetter(property, addImport)) + .map(getter => + getter + .split('\n') + .map(line => `${indent}${line}`) + .join('\n'), + ) + .join('\n'); + const imports = [...importSet] + .map($import => `import ${$import};`) + .sort() + .join('\n'); + return `/** +* 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. +* +* ${'@'}generated by codegen project: GeneratePropsJavaPojo.js +*/ + +package ${basePackageName}.${pojo.namespace}; +${imports === '' ? '' : `\n${imports}\n`} +@DoNotStrip +public class ${pojo.name} { +${members} +${getters} +} +`; +} +module.exports = { + serializePojo, +}; diff --git a/packages/react-native-codegen/lib/generators/components/GeneratePropsJavaPojo/serializePojo.js.flow b/packages/react-native-codegen/lib/generators/components/GeneratePropsJavaPojo/serializePojo.js.flow new file mode 100644 index 00000000000000..7ca6df11d081cd --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/GeneratePropsJavaPojo/serializePojo.js.flow @@ -0,0 +1,315 @@ +/** + * 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 {Pojo, PojoProperty, PojoTypeAnnotation} from './PojoCollector'; + +const {capitalize} = require('../../Utils'); + +type ImportCollector = ($import: string) => void; + +function toJavaType( + typeAnnotation: PojoTypeAnnotation, + addImport: ImportCollector, +): string { + const importNullable = () => addImport('androidx.annotation.Nullable'); + const importReadableMap = () => + addImport('com.facebook.react.bridge.ReadableMap'); + const importArrayList = () => addImport('java.util.ArrayList'); + const importYogaValue = () => addImport('com.facebook.yoga.YogaValue'); + const importDynamic = () => addImport('com.facebook.react.bridge.Dynamic'); + switch (typeAnnotation.type) { + /** + * Primitives + */ + case 'BooleanTypeAnnotation': { + if (typeAnnotation.default === null) { + importNullable(); + return '@Nullable Boolean'; + } else { + return 'boolean'; + } + } + case 'StringTypeAnnotation': { + importNullable(); + return '@Nullable String'; + } + case 'DoubleTypeAnnotation': { + return 'double'; + } + case 'FloatTypeAnnotation': { + if (typeAnnotation.default === null) { + importNullable(); + return '@Nullable Float'; + } else { + return 'float'; + } + } + case 'Int32TypeAnnotation': { + return 'int'; + } + + /** + * Enums + */ + // TODO: Make StringEnumTypeAnnotation type-safe? + case 'StringEnumTypeAnnotation': + importNullable(); + return '@Nullable String'; + // TODO: Make Int32EnumTypeAnnotation type-safe? + case 'Int32EnumTypeAnnotation': + importNullable(); + return '@Nullable Integer'; + + /** + * Reserved types + */ + case 'ReservedPropTypeAnnotation': { + switch (typeAnnotation.name) { + case 'ColorPrimitive': + importNullable(); + return '@Nullable Integer'; + + // TODO: Make ImageSourcePrimitive type-safe + case 'ImageSourcePrimitive': + importNullable(); + importReadableMap(); + return '@Nullable ReadableMap'; + + // TODO: Make ImageRequestPrimitive type-safe + case 'ImageRequestPrimitive': + importNullable(); + importReadableMap(); + return '@Nullable ReadableMap'; + + // TODO: Make PointPrimitive type-safe + case 'PointPrimitive': + importNullable(); + importReadableMap(); + return '@Nullable ReadableMap'; + + // TODO: Make EdgeInsetsPrimitive type-safe + case 'EdgeInsetsPrimitive': + importNullable(); + importReadableMap(); + return '@Nullable ReadableMap'; + + case 'DimensionPrimitive': + importNullable(); + importYogaValue(); + return '@Nullable YogaValue'; + + default: + (typeAnnotation.name: empty); + throw new Error( + `Received unknown ReservedPropTypeAnnotation ${typeAnnotation.name}`, + ); + } + } + + /** + * Other Pojo objects + */ + case 'PojoTypeAliasTypeAnnotation': { + return typeAnnotation.name; + } + + /** + * Arrays + */ + case 'ArrayTypeAnnotation': { + const {elementType} = typeAnnotation; + + const elementTypeString = (() => { + switch (elementType.type) { + /** + * Primitives + */ + case 'BooleanTypeAnnotation': { + return 'Boolean'; + } + case 'StringTypeAnnotation': { + return 'String'; + } + case 'DoubleTypeAnnotation': { + return 'Double'; + } + case 'FloatTypeAnnotation': { + return 'Float'; + } + case 'Int32TypeAnnotation': { + return 'Integer'; + } + + /** + * Enums + */ + // TODO: Make StringEnums type-safe in Pojos + case 'StringEnumTypeAnnotation': { + return 'String'; + } + + /** + * Other Pojo objects + */ + case 'PojoTypeAliasTypeAnnotation': { + return elementType.name; + } + + /** + * Reserved types + */ + case 'ReservedPropTypeAnnotation': { + switch (elementType.name) { + case 'ColorPrimitive': + return 'Integer'; + + // TODO: Make ImageSourcePrimitive type-safe + case 'ImageSourcePrimitive': + importReadableMap(); + return 'ReadableMap'; + + // TODO: Make ImageRequestPrimitive type-safe + case 'ImageRequestPrimitive': + importReadableMap(); + return 'ReadableMap'; + + // TODO: Make PointPrimitive type-safe + case 'PointPrimitive': + importReadableMap(); + return 'ReadableMap'; + + // TODO: Make EdgeInsetsPrimitive type-safe + case 'EdgeInsetsPrimitive': + importReadableMap(); + return 'ReadableMap'; + + case 'DimensionPrimitive': + importYogaValue(); + return 'YogaValue'; + + default: + (elementType.name: empty); + throw new Error( + `Received unknown ReservedPropTypeAnnotation ${elementType.name}`, + ); + } + } + + // Arrays + case 'ArrayTypeAnnotation': { + const {elementType: pojoTypeAliasTypeAnnotation} = elementType; + + importArrayList(); + return `ArrayList<${pojoTypeAliasTypeAnnotation.name}>`; + } + default: { + (elementType.type: empty); + throw new Error( + `Unrecognized PojoTypeAnnotation Array element type annotation '${typeAnnotation.type}'`, + ); + } + } + })(); + + importArrayList(); + return `ArrayList<${elementTypeString}>`; + } + + case 'MixedTypeAnnotation': { + importDynamic(); + return 'Dynamic'; + } + + default: { + (typeAnnotation.type: empty); + throw new Error( + `Unrecognized PojoTypeAnnotation '${typeAnnotation.type}'`, + ); + } + } +} + +function toJavaMemberName(property: PojoProperty): string { + return `m${capitalize(property.name)}`; +} + +function toJavaMemberDeclaration( + property: PojoProperty, + addImport: ImportCollector, +): string { + const type = toJavaType(property.typeAnnotation, addImport); + const memberName = toJavaMemberName(property); + return `private ${type} ${memberName};`; +} + +function toJavaGetter(property: PojoProperty, addImport: ImportCollector) { + const type = toJavaType(property.typeAnnotation, addImport); + const getterName = `get${capitalize(property.name)}`; + const memberName = toJavaMemberName(property); + + addImport('com.facebook.proguard.annotations.DoNotStrip'); + return `@DoNotStrip +public ${type} ${getterName}() { + return ${memberName}; +}`; +} + +function serializePojo(pojo: Pojo, basePackageName: string): string { + const importSet: Set = new Set(); + const addImport = ($import: string) => { + importSet.add($import); + }; + + addImport('com.facebook.proguard.annotations.DoNotStrip'); + + const indent = ' '.repeat(2); + + const members = pojo.properties + .map(property => toJavaMemberDeclaration(property, addImport)) + .map(member => `${indent}${member}`) + .join('\n'); + + const getters = pojo.properties + .map(property => toJavaGetter(property, addImport)) + .map(getter => + getter + .split('\n') + .map(line => `${indent}${line}`) + .join('\n'), + ) + .join('\n'); + + const imports = [...importSet] + .map($import => `import ${$import};`) + .sort() + .join('\n'); + + return `/** +* 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. +* +* ${'@'}generated by codegen project: GeneratePropsJavaPojo.js +*/ + +package ${basePackageName}.${pojo.namespace}; +${imports === '' ? '' : `\n${imports}\n`} +@DoNotStrip +public class ${pojo.name} { +${members} +${getters} +} +`; +} + +module.exports = {serializePojo}; diff --git a/packages/react-native-codegen/lib/generators/components/GenerateShadowNodeCpp.js b/packages/react-native-codegen/lib/generators/components/GenerateShadowNodeCpp.js new file mode 100644 index 00000000000000..cf91c7944bc99d --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/GenerateShadowNodeCpp.js @@ -0,0 +1,84 @@ +/** + * 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 _require = require('./CppHelpers'), + IncludeTemplate = _require.IncludeTemplate; + +// File path -> contents + +const FileTemplate = ({componentNames, headerPrefix}) => ` +/** + * 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: GenerateShadowNodeCpp.js + */ + +${IncludeTemplate({ + headerPrefix, + file: 'ShadowNodes.h', +})} + +namespace facebook::react { + +${componentNames} + +} // namespace facebook::react +`; +const ComponentTemplate = ({className}) => + ` +extern const char ${className}ComponentName[] = "${className}"; +`.trim(); +module.exports = { + generate( + libraryName, + schema, + packageName, + assumeNonnull = false, + headerPrefix, + ) { + const fileName = 'ShadowNodes.cpp'; + const componentNames = Object.keys(schema.modules) + .map(moduleName => { + const module = schema.modules[moduleName]; + if (module.type !== 'Component') { + return; + } + const components = module.components; + // No components in this module + if (components == null) { + return null; + } + return Object.keys(components) + .map(componentName => { + if (components[componentName].interfaceOnly === true) { + return; + } + const replacedTemplate = ComponentTemplate({ + className: componentName, + }); + return replacedTemplate; + }) + .join('\n'); + }) + .filter(Boolean) + .join('\n'); + const replacedTemplate = FileTemplate({ + componentNames, + headerPrefix: + headerPrefix !== null && headerPrefix !== void 0 ? headerPrefix : '', + }); + return new Map([[fileName, replacedTemplate]]); + }, +}; diff --git a/packages/react-native-codegen/lib/generators/components/GenerateShadowNodeCpp.js.flow b/packages/react-native-codegen/lib/generators/components/GenerateShadowNodeCpp.js.flow new file mode 100644 index 00000000000000..c6d9f7b738f608 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/GenerateShadowNodeCpp.js.flow @@ -0,0 +1,96 @@ +/** + * 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 {SchemaType} from '../../CodegenSchema'; + +const {IncludeTemplate} = require('./CppHelpers'); + +// File path -> contents +type FilesOutput = Map; + +const FileTemplate = ({ + componentNames, + headerPrefix, +}: { + componentNames: string, + headerPrefix: 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: GenerateShadowNodeCpp.js + */ + +${IncludeTemplate({headerPrefix, file: 'ShadowNodes.h'})} + +namespace facebook::react { + +${componentNames} + +} // namespace facebook::react +`; + +const ComponentTemplate = ({className}: {className: string}) => + ` +extern const char ${className}ComponentName[] = "${className}"; +`.trim(); + +module.exports = { + generate( + libraryName: string, + schema: SchemaType, + packageName?: string, + assumeNonnull: boolean = false, + headerPrefix?: string, + ): FilesOutput { + const fileName = 'ShadowNodes.cpp'; + + const componentNames = 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 => { + if (components[componentName].interfaceOnly === true) { + return; + } + const replacedTemplate = ComponentTemplate({ + className: componentName, + }); + + return replacedTemplate; + }) + .join('\n'); + }) + .filter(Boolean) + .join('\n'); + + const replacedTemplate = FileTemplate({ + componentNames, + headerPrefix: headerPrefix ?? '', + }); + + return new Map([[fileName, replacedTemplate]]); + }, +}; diff --git a/packages/react-native-codegen/lib/generators/components/GenerateShadowNodeH.js b/packages/react-native-codegen/lib/generators/components/GenerateShadowNodeH.js new file mode 100644 index 00000000000000..6e23377bb8738b --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/GenerateShadowNodeH.js @@ -0,0 +1,107 @@ +/** + * 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 _require = require('./CppHelpers'), + IncludeTemplate = _require.IncludeTemplate; + +// File path -> contents + +const FileTemplate = ({componentClasses, headerPrefix}) => ` +/** + * 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: GenerateShadowNodeH.js + */ + +#pragma once + +${IncludeTemplate({ + headerPrefix, + file: 'EventEmitters.h', +})} +${IncludeTemplate({ + headerPrefix, + file: 'Props.h', +})} +${IncludeTemplate({ + headerPrefix, + file: 'States.h', +})} +#include +#include + +namespace facebook::react { + +${componentClasses} + +} // namespace facebook::react +`; +const ComponentTemplate = ({className, eventEmitter}) => + ` +JSI_EXPORT extern const char ${className}ComponentName[]; + +/* + * \`ShadowNode\` for <${className}> component. + */ +using ${className}ShadowNode = ConcreteViewShadowNode< + ${className}ComponentName, + ${className}Props${eventEmitter}, + ${className}State>; +`.trim(); +module.exports = { + generate( + libraryName, + schema, + packageName, + assumeNonnull = false, + headerPrefix, + ) { + const fileName = 'ShadowNodes.h'; + const moduleResults = Object.keys(schema.modules) + .map(moduleName => { + const module = schema.modules[moduleName]; + if (module.type !== 'Component') { + return; + } + const components = module.components; + // No components in this module + if (components == null) { + return null; + } + return Object.keys(components) + .map(componentName => { + const component = components[componentName]; + if (component.interfaceOnly === true) { + return; + } + const eventEmitter = `,\n ${componentName}EventEmitter`; + const replacedTemplate = ComponentTemplate({ + className: componentName, + eventEmitter, + }); + return replacedTemplate; + }) + .join('\n\n'); + }) + .filter(Boolean) + .join('\n\n'); + const replacedTemplate = FileTemplate({ + componentClasses: moduleResults, + headerPrefix: + headerPrefix !== null && headerPrefix !== void 0 ? headerPrefix : '', + }); + return new Map([[fileName, replacedTemplate]]); + }, +}; diff --git a/packages/react-native-codegen/lib/generators/components/GenerateShadowNodeH.js.flow b/packages/react-native-codegen/lib/generators/components/GenerateShadowNodeH.js.flow new file mode 100644 index 00000000000000..8218495e940073 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/GenerateShadowNodeH.js.flow @@ -0,0 +1,121 @@ +/** + * 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 {SchemaType} from '../../CodegenSchema'; + +const {IncludeTemplate} = require('./CppHelpers'); + +// File path -> contents +type FilesOutput = Map; + +const FileTemplate = ({ + componentClasses, + headerPrefix, +}: { + componentClasses: string, + headerPrefix: 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: GenerateShadowNodeH.js + */ + +#pragma once + +${IncludeTemplate({headerPrefix, file: 'EventEmitters.h'})} +${IncludeTemplate({headerPrefix, file: 'Props.h'})} +${IncludeTemplate({headerPrefix, file: 'States.h'})} +#include +#include + +namespace facebook::react { + +${componentClasses} + +} // namespace facebook::react +`; + +const ComponentTemplate = ({ + className, + eventEmitter, +}: { + className: string, + eventEmitter: string, +}) => + ` +JSI_EXPORT extern const char ${className}ComponentName[]; + +/* + * \`ShadowNode\` for <${className}> component. + */ +using ${className}ShadowNode = ConcreteViewShadowNode< + ${className}ComponentName, + ${className}Props${eventEmitter}, + ${className}State>; +`.trim(); + +module.exports = { + generate( + libraryName: string, + schema: SchemaType, + packageName?: string, + assumeNonnull: boolean = false, + headerPrefix?: string, + ): FilesOutput { + const fileName = 'ShadowNodes.h'; + + const moduleResults = 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]; + if (component.interfaceOnly === true) { + return; + } + + const eventEmitter = `,\n ${componentName}EventEmitter`; + + const replacedTemplate = ComponentTemplate({ + className: componentName, + eventEmitter, + }); + + return replacedTemplate; + }) + .join('\n\n'); + }) + .filter(Boolean) + .join('\n\n'); + + const replacedTemplate = FileTemplate({ + componentClasses: moduleResults, + headerPrefix: headerPrefix ?? '', + }); + + return new Map([[fileName, replacedTemplate]]); + }, +}; diff --git a/packages/react-native-codegen/lib/generators/components/GenerateStateCpp.js b/packages/react-native-codegen/lib/generators/components/GenerateStateCpp.js new file mode 100644 index 00000000000000..1898a4c49d7e4e --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/GenerateStateCpp.js @@ -0,0 +1,81 @@ +/** + * 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 _require = require('./CppHelpers'), + IncludeTemplate = _require.IncludeTemplate; + +// File path -> contents + +const FileTemplate = ({stateClasses, headerPrefix}) => ` +/** + * 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: GenerateStateCpp.js + */ +${IncludeTemplate({ + headerPrefix, + file: 'States.h', +})} + +namespace facebook::react { + +${stateClasses} + +} // namespace facebook::react +`; +const StateTemplate = ({stateName}) => ''; +module.exports = { + generate( + libraryName, + schema, + packageName, + assumeNonnull = false, + headerPrefix, + ) { + const fileName = 'States.cpp'; + const stateClasses = Object.keys(schema.modules) + .map(moduleName => { + const module = schema.modules[moduleName]; + if (module.type !== 'Component') { + return; + } + const components = module.components; + // No components in this module + if (components == null) { + return null; + } + return Object.keys(components) + .map(componentName => { + const component = components[componentName]; + if (component.interfaceOnly === true) { + return null; + } + return StateTemplate({ + stateName: `${componentName}State`, + }); + }) + .filter(Boolean) + .join('\n'); + }) + .filter(Boolean) + .join('\n'); + const replacedTemplate = FileTemplate({ + stateClasses, + headerPrefix: + headerPrefix !== null && headerPrefix !== void 0 ? headerPrefix : '', + }); + return new Map([[fileName, replacedTemplate]]); + }, +}; diff --git a/packages/react-native-codegen/lib/generators/components/GenerateStateCpp.js.flow b/packages/react-native-codegen/lib/generators/components/GenerateStateCpp.js.flow new file mode 100644 index 00000000000000..87d734a10a7aeb --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/GenerateStateCpp.js.flow @@ -0,0 +1,93 @@ +/** + * 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 {SchemaType} from '../../CodegenSchema'; + +const {IncludeTemplate} = require('./CppHelpers'); + +// File path -> contents +type FilesOutput = Map; + +const FileTemplate = ({ + stateClasses, + headerPrefix, +}: { + stateClasses: string, + headerPrefix: 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: GenerateStateCpp.js + */ +${IncludeTemplate({headerPrefix, file: 'States.h'})} + +namespace facebook::react { + +${stateClasses} + +} // namespace facebook::react +`; + +const StateTemplate = ({stateName}: {stateName: string}) => ''; + +module.exports = { + generate( + libraryName: string, + schema: SchemaType, + packageName?: string, + assumeNonnull: boolean = false, + headerPrefix?: string, + ): FilesOutput { + const fileName = 'States.cpp'; + + const stateClasses = 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]; + if (component.interfaceOnly === true) { + return null; + } + + return StateTemplate({ + stateName: `${componentName}State`, + }); + }) + .filter(Boolean) + .join('\n'); + }) + .filter(Boolean) + .join('\n'); + + const replacedTemplate = FileTemplate({ + stateClasses, + headerPrefix: headerPrefix ?? '', + }); + + return new Map([[fileName, replacedTemplate]]); + }, +}; diff --git a/packages/react-native-codegen/lib/generators/components/GenerateStateH.js b/packages/react-native-codegen/lib/generators/components/GenerateStateH.js new file mode 100644 index 00000000000000..ab9e66a5408e37 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/GenerateStateH.js @@ -0,0 +1,92 @@ +/** + * 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'; + +// File path -> contents + +const FileTemplate = ({libraryName, stateClasses}) => + ` +/** + * 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: GenerateStateH.js + */ +#pragma once + +#ifdef ANDROID +#include +#endif + +namespace facebook::react { + +${stateClasses} + +} // namespace facebook::react +`.trim(); +const StateTemplate = ({stateName}) => + ` +class ${stateName}State { +public: + ${stateName}State() = default; + +#ifdef ANDROID + ${stateName}State(${stateName}State const &previousState, folly::dynamic data){}; + folly::dynamic getDynamic() const { + return {}; + }; +#endif +}; +`.trim(); +module.exports = { + generate( + libraryName, + schema, + packageName, + assumeNonnull = false, + headerPrefix, + ) { + const fileName = 'States.h'; + const stateClasses = Object.keys(schema.modules) + .map(moduleName => { + const module = schema.modules[moduleName]; + if (module.type !== 'Component') { + return; + } + const components = module.components; + // No components in this module + if (components == null) { + return null; + } + return Object.keys(components) + .map(componentName => { + const component = components[componentName]; + if (component.interfaceOnly === true) { + return null; + } + return StateTemplate({ + stateName: componentName, + }); + }) + .filter(Boolean) + .join('\n\n'); + }) + .filter(Boolean) + .join('\n\n'); + const template = FileTemplate({ + libraryName, + stateClasses, + }); + return new Map([[fileName, template]]); + }, +}; diff --git a/packages/react-native-codegen/lib/generators/components/GenerateStateH.js.flow b/packages/react-native-codegen/lib/generators/components/GenerateStateH.js.flow new file mode 100644 index 00000000000000..8805977275f93e --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/GenerateStateH.js.flow @@ -0,0 +1,105 @@ +/** + * 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 {SchemaType} from '../../CodegenSchema'; + +// File path -> contents +type FilesOutput = Map; + +const FileTemplate = ({ + libraryName, + stateClasses, +}: { + libraryName: string, + stateClasses: 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: GenerateStateH.js + */ +#pragma once + +#ifdef ANDROID +#include +#endif + +namespace facebook::react { + +${stateClasses} + +} // namespace facebook::react +`.trim(); + +const StateTemplate = ({stateName}: {stateName: string}) => + ` +class ${stateName}State { +public: + ${stateName}State() = default; + +#ifdef ANDROID + ${stateName}State(${stateName}State const &previousState, folly::dynamic data){}; + folly::dynamic getDynamic() const { + return {}; + }; +#endif +}; +`.trim(); + +module.exports = { + generate( + libraryName: string, + schema: SchemaType, + packageName?: string, + assumeNonnull: boolean = false, + headerPrefix?: string, + ): FilesOutput { + const fileName = 'States.h'; + + const stateClasses = 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]; + if (component.interfaceOnly === true) { + return null; + } + return StateTemplate({stateName: componentName}); + }) + .filter(Boolean) + .join('\n\n'); + }) + .filter(Boolean) + .join('\n\n'); + + const template = FileTemplate({ + libraryName, + stateClasses, + }); + return new Map([[fileName, template]]); + }, +}; diff --git a/packages/react-native-codegen/lib/generators/components/GenerateTests.js b/packages/react-native-codegen/lib/generators/components/GenerateTests.js new file mode 100644 index 00000000000000..a172a8b7c33846 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/GenerateTests.js @@ -0,0 +1,172 @@ +/** + * 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 _require = require('../Utils'), + toSafeCppString = _require.toSafeCppString; +const _require2 = require('./CppHelpers'), + getImports = _require2.getImports; +const FileTemplate = ({libraryName, imports, componentTests}) => + ` +/** + * 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: GenerateTests.js + * */ + +#include +#include +#include +${imports} + +using namespace facebook::react; +${componentTests} +`.trim(); +const TestTemplate = ({componentName, testName, propName, propValue}) => ` +TEST(${componentName}_${testName}, etc) { + auto propParser = RawPropsParser(); + propParser.prepare<${componentName}>(); + auto const &sourceProps = ${componentName}(); + auto const &rawProps = RawProps(folly::dynamic::object("${propName}", ${propValue})); + + ContextContainer contextContainer{}; + PropsParserContext parserContext{-1, contextContainer}; + + rawProps.parse(propParser, parserContext); + ${componentName}(parserContext, sourceProps, rawProps); +} +`; +function getTestCasesForProp(propName, typeAnnotation) { + const cases = []; + if (typeAnnotation.type === 'StringEnumTypeAnnotation') { + typeAnnotation.options.forEach(option => + cases.push({ + propName, + testName: `${propName}_${toSafeCppString(option)}`, + propValue: option, + }), + ); + } else if (typeAnnotation.type === 'StringTypeAnnotation') { + cases.push({ + propName, + propValue: + typeAnnotation.default != null && typeAnnotation.default !== '' + ? typeAnnotation.default + : 'foo', + }); + } else if (typeAnnotation.type === 'BooleanTypeAnnotation') { + cases.push({ + propName: propName, + propValue: typeAnnotation.default != null ? typeAnnotation.default : true, + }); + // $FlowFixMe[incompatible-type] + } else if (typeAnnotation.type === 'IntegerTypeAnnotation') { + cases.push({ + propName, + propValue: typeAnnotation.default || 10, + }); + } else if (typeAnnotation.type === 'FloatTypeAnnotation') { + cases.push({ + propName, + propValue: typeAnnotation.default != null ? typeAnnotation.default : 0.1, + }); + } else if (typeAnnotation.type === 'ReservedPropTypeAnnotation') { + if (typeAnnotation.name === 'ColorPrimitive') { + cases.push({ + propName, + propValue: 1, + }); + } else if (typeAnnotation.name === 'PointPrimitive') { + cases.push({ + propName, + propValue: 'folly::dynamic::object("x", 1)("y", 1)', + raw: true, + }); + } else if (typeAnnotation.name === 'ImageSourcePrimitive') { + cases.push({ + propName, + propValue: 'folly::dynamic::object("url", "testurl")', + raw: true, + }); + } + } + return cases; +} +function generateTestsString(name, component) { + function createTest({testName, propName, propValue, raw = false}) { + const value = + !raw && typeof propValue === 'string' ? `"${propValue}"` : propValue; + return TestTemplate({ + componentName: name, + testName: testName != null ? testName : propName, + propName, + propValue: String(value), + }); + } + const testCases = component.props.reduce((cases, prop) => { + return cases.concat(getTestCasesForProp(prop.name, prop.typeAnnotation)); + }, []); + const baseTest = { + testName: 'DoesNotDie', + propName: 'xx_invalid_xx', + propValue: 'xx_invalid_xx', + }; + return [baseTest, ...testCases].map(createTest).join(''); +} +module.exports = { + generate( + libraryName, + schema, + packageName, + assumeNonnull = false, + headerPrefix, + ) { + const fileName = 'Tests.cpp'; + const allImports = new Set([ + '#include ', + '#include ', + '#include ', + ]); + const componentTests = Object.keys(schema.modules) + .map(moduleName => { + const module = schema.modules[moduleName]; + if (module.type !== 'Component') { + return; + } + const components = module.components; + if (components == null) { + return null; + } + return Object.keys(components) + .map(componentName => { + const component = components[componentName]; + const name = `${componentName}Props`; + const imports = getImports(component.props); + // $FlowFixMe[method-unbinding] added when improving typing for this parameters + imports.forEach(allImports.add, allImports); + return generateTestsString(name, component); + }) + .join(''); + }) + .filter(Boolean) + .join(''); + const imports = Array.from(allImports).sort().join('\n').trim(); + const replacedTemplate = FileTemplate({ + imports, + libraryName, + componentTests, + }); + return new Map([[fileName, replacedTemplate]]); + }, +}; diff --git a/packages/react-native-codegen/lib/generators/components/GenerateTests.js.flow b/packages/react-native-codegen/lib/generators/components/GenerateTests.js.flow new file mode 100644 index 00000000000000..8017f933ef4467 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/GenerateTests.js.flow @@ -0,0 +1,221 @@ +/** + * 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, PropTypeAnnotation} from '../../CodegenSchema'; +import type {SchemaType} from '../../CodegenSchema'; + +const {toSafeCppString} = require('../Utils'); +const {getImports} = require('./CppHelpers'); + +type FilesOutput = Map; +type PropValueType = string | number | boolean; + +type TestCase = $ReadOnly<{ + propName: string, + propValue: ?PropValueType, + testName?: string, + raw?: boolean, +}>; + +const FileTemplate = ({ + libraryName, + imports, + componentTests, +}: { + libraryName: string, + imports: string, + componentTests: 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: GenerateTests.js + * */ + +#include +#include +#include +${imports} + +using namespace facebook::react; +${componentTests} +`.trim(); + +const TestTemplate = ({ + componentName, + testName, + propName, + propValue, +}: { + componentName: string, + testName: string, + propName: string, + propValue: string, +}) => ` +TEST(${componentName}_${testName}, etc) { + auto propParser = RawPropsParser(); + propParser.prepare<${componentName}>(); + auto const &sourceProps = ${componentName}(); + auto const &rawProps = RawProps(folly::dynamic::object("${propName}", ${propValue})); + + ContextContainer contextContainer{}; + PropsParserContext parserContext{-1, contextContainer}; + + rawProps.parse(propParser, parserContext); + ${componentName}(parserContext, sourceProps, rawProps); +} +`; + +function getTestCasesForProp( + propName: string, + typeAnnotation: PropTypeAnnotation, +) { + const cases = []; + if (typeAnnotation.type === 'StringEnumTypeAnnotation') { + typeAnnotation.options.forEach(option => + cases.push({ + propName, + testName: `${propName}_${toSafeCppString(option)}`, + propValue: option, + }), + ); + } else if (typeAnnotation.type === 'StringTypeAnnotation') { + cases.push({ + propName, + propValue: + typeAnnotation.default != null && typeAnnotation.default !== '' + ? typeAnnotation.default + : 'foo', + }); + } else if (typeAnnotation.type === 'BooleanTypeAnnotation') { + cases.push({ + propName: propName, + propValue: typeAnnotation.default != null ? typeAnnotation.default : true, + }); + // $FlowFixMe[incompatible-type] + } else if (typeAnnotation.type === 'IntegerTypeAnnotation') { + cases.push({ + propName, + propValue: typeAnnotation.default || 10, + }); + } else if (typeAnnotation.type === 'FloatTypeAnnotation') { + cases.push({ + propName, + propValue: typeAnnotation.default != null ? typeAnnotation.default : 0.1, + }); + } else if (typeAnnotation.type === 'ReservedPropTypeAnnotation') { + if (typeAnnotation.name === 'ColorPrimitive') { + cases.push({ + propName, + propValue: 1, + }); + } else if (typeAnnotation.name === 'PointPrimitive') { + cases.push({ + propName, + propValue: 'folly::dynamic::object("x", 1)("y", 1)', + raw: true, + }); + } else if (typeAnnotation.name === 'ImageSourcePrimitive') { + cases.push({ + propName, + propValue: 'folly::dynamic::object("url", "testurl")', + raw: true, + }); + } + } + + return cases; +} + +function generateTestsString(name: string, component: ComponentShape) { + function createTest({testName, propName, propValue, raw = false}: TestCase) { + const value = + !raw && typeof propValue === 'string' ? `"${propValue}"` : propValue; + + return TestTemplate({ + componentName: name, + testName: testName != null ? testName : propName, + propName, + propValue: String(value), + }); + } + + const testCases = component.props.reduce((cases: Array, prop) => { + return cases.concat(getTestCasesForProp(prop.name, prop.typeAnnotation)); + }, []); + + const baseTest = { + testName: 'DoesNotDie', + propName: 'xx_invalid_xx', + propValue: 'xx_invalid_xx', + }; + + return [baseTest, ...testCases].map(createTest).join(''); +} + +module.exports = { + generate( + libraryName: string, + schema: SchemaType, + packageName?: string, + assumeNonnull: boolean = false, + headerPrefix?: string, + ): FilesOutput { + const fileName = 'Tests.cpp'; + const allImports = new Set([ + '#include ', + '#include ', + '#include ', + ]); + + const componentTests = Object.keys(schema.modules) + .map(moduleName => { + const module = schema.modules[moduleName]; + if (module.type !== 'Component') { + return; + } + + const {components} = module; + if (components == null) { + return null; + } + + return Object.keys(components) + .map(componentName => { + const component = components[componentName]; + const name = `${componentName}Props`; + + const imports = getImports(component.props); + // $FlowFixMe[method-unbinding] added when improving typing for this parameters + imports.forEach(allImports.add, allImports); + + return generateTestsString(name, component); + }) + .join(''); + }) + .filter(Boolean) + .join(''); + + const imports = Array.from(allImports).sort().join('\n').trim(); + + const replacedTemplate = FileTemplate({ + imports, + libraryName, + componentTests, + }); + + return new Map([[fileName, replacedTemplate]]); + }, +}; diff --git a/packages/react-native-codegen/lib/generators/components/GenerateThirdPartyFabricComponentsProviderH.js b/packages/react-native-codegen/lib/generators/components/GenerateThirdPartyFabricComponentsProviderH.js new file mode 100644 index 00000000000000..3c87312b4bc038 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/GenerateThirdPartyFabricComponentsProviderH.js @@ -0,0 +1,108 @@ +/** + * 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 _require = require('./ComponentsProviderUtils'), + generateSupportedApplePlatformsMacro = + _require.generateSupportedApplePlatformsMacro; + +// File path -> contents + +const FileTemplate = ({lookupFuncs}) => ` +/* + * 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 GenerateRCTThirdPartyFabricComponentsProviderH + */ + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wreturn-type-c-linkage" + +#import + +#ifdef __cplusplus +extern "C" { +#endif + +Class RCTThirdPartyFabricComponentsProvider(const char *name); +#if RCT_NEW_ARCH_ENABLED +#ifndef RCT_DYNAMIC_FRAMEWORKS + +${lookupFuncs} + +#endif +#endif + +#ifdef __cplusplus +} +#endif + +#pragma GCC diagnostic pop + +`; +const LookupFuncTemplate = ({className, libraryName}) => + ` +Class ${className}Cls(void) __attribute__((used)); // ${libraryName} +`.trim(); +module.exports = { + generate(schemas, supportedApplePlatforms) { + const fileName = 'RCTThirdPartyFabricComponentsProvider.h'; + const lookupFuncs = Object.keys(schemas) + .map(libraryName => { + const schema = schemas[libraryName]; + const librarySupportedApplePlatforms = + supportedApplePlatforms === null || supportedApplePlatforms === void 0 + ? void 0 + : supportedApplePlatforms[libraryName]; + const generatedLookup = Object.keys(schema.modules) + .map(moduleName => { + const module = schema.modules[moduleName]; + if (module.type !== 'Component') { + return; + } + const components = module.components; + // No components in this module + if (components == null) { + return null; + } + return Object.keys(components) + .filter(componentName => { + const component = components[componentName]; + return !( + component.excludedPlatforms && + component.excludedPlatforms.includes('iOS') + ); + }) + .map(componentName => { + return LookupFuncTemplate({ + className: componentName, + libraryName, + }); + }) + .join('\n'); + }) + .filter(Boolean) + .join('\n'); + return generateSupportedApplePlatformsMacro( + generatedLookup, + librarySupportedApplePlatforms, + ); + }) + .join('\n'); + const replacedTemplate = FileTemplate({ + lookupFuncs, + }); + return new Map([[fileName, replacedTemplate]]); + }, +}; diff --git a/packages/react-native-codegen/lib/generators/components/GenerateThirdPartyFabricComponentsProviderH.js.flow b/packages/react-native-codegen/lib/generators/components/GenerateThirdPartyFabricComponentsProviderH.js.flow new file mode 100644 index 00000000000000..cc42edb5a04b1e --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/GenerateThirdPartyFabricComponentsProviderH.js.flow @@ -0,0 +1,126 @@ +/** + * 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 {SchemaType} from '../../CodegenSchema'; + +const { + generateSupportedApplePlatformsMacro, +} = require('./ComponentsProviderUtils'); + +// File path -> contents +type FilesOutput = Map; + +const FileTemplate = ({lookupFuncs}: {lookupFuncs: 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 GenerateRCTThirdPartyFabricComponentsProviderH + */ + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wreturn-type-c-linkage" + +#import + +#ifdef __cplusplus +extern "C" { +#endif + +Class RCTThirdPartyFabricComponentsProvider(const char *name); +#if RCT_NEW_ARCH_ENABLED +#ifndef RCT_DYNAMIC_FRAMEWORKS + +${lookupFuncs} + +#endif +#endif + +#ifdef __cplusplus +} +#endif + +#pragma GCC diagnostic pop + +`; + +const LookupFuncTemplate = ({ + className, + libraryName, +}: { + className: string, + libraryName: string, +}) => + ` +Class ${className}Cls(void) __attribute__((used)); // ${libraryName} +`.trim(); + +module.exports = { + generate( + schemas: {[string]: SchemaType}, + supportedApplePlatforms?: {[string]: {[string]: boolean}}, + ): FilesOutput { + const fileName = 'RCTThirdPartyFabricComponentsProvider.h'; + + const lookupFuncs = Object.keys(schemas) + .map(libraryName => { + const schema = schemas[libraryName]; + const librarySupportedApplePlatforms = + supportedApplePlatforms?.[libraryName]; + const generatedLookup = 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) + .filter(componentName => { + const component = components[componentName]; + return !( + component.excludedPlatforms && + component.excludedPlatforms.includes('iOS') + ); + }) + .map(componentName => { + return LookupFuncTemplate({ + className: componentName, + libraryName, + }); + }) + .join('\n'); + }) + .filter(Boolean) + .join('\n'); + + return generateSupportedApplePlatformsMacro( + generatedLookup, + librarySupportedApplePlatforms, + ); + }) + .join('\n'); + + const replacedTemplate = FileTemplate({ + lookupFuncs, + }); + + return new Map([[fileName, replacedTemplate]]); + }, +}; diff --git a/packages/react-native-codegen/lib/generators/components/GenerateThirdPartyFabricComponentsProviderObjCpp.js b/packages/react-native-codegen/lib/generators/components/GenerateThirdPartyFabricComponentsProviderObjCpp.js new file mode 100644 index 00000000000000..818d89c80e90c9 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/GenerateThirdPartyFabricComponentsProviderObjCpp.js @@ -0,0 +1,106 @@ +/** + * 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 _require = require('./ComponentsProviderUtils'), + generateSupportedApplePlatformsMacro = + _require.generateSupportedApplePlatformsMacro; + +// File path -> contents + +const FileTemplate = ({lookupMap}) => ` +/** + * 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 GenerateRCTThirdPartyFabricComponentsProviderCpp + */ + +// OSS-compatibility layer + +#import "RCTThirdPartyFabricComponentsProvider.h" + +#import +#import + +Class RCTThirdPartyFabricComponentsProvider(const char *name) { + static std::unordered_map sFabricComponentsClassMap = { + #if RCT_NEW_ARCH_ENABLED + #ifndef RCT_DYNAMIC_FRAMEWORKS +${lookupMap} + #endif + #endif + }; + + auto p = sFabricComponentsClassMap.find(name); + if (p != sFabricComponentsClassMap.end()) { + auto classFunc = p->second; + return classFunc(); + } + return nil; +} +`; +const LookupMapTemplate = ({className, libraryName}) => ` + {"${className}", ${className}Cls}, // ${libraryName}`; +module.exports = { + generate(schemas, supportedApplePlatforms) { + const fileName = 'RCTThirdPartyFabricComponentsProvider.mm'; + const lookupMap = Object.keys(schemas) + .map(libraryName => { + const schema = schemas[libraryName]; + const librarySupportedApplePlatforms = + supportedApplePlatforms === null || supportedApplePlatforms === void 0 + ? void 0 + : supportedApplePlatforms[libraryName]; + const generatedLookup = Object.keys(schema.modules) + .map(moduleName => { + const module = schema.modules[moduleName]; + if (module.type !== 'Component') { + return; + } + const components = module.components; + // No components in this module + if (components == null) { + return null; + } + const componentTemplates = Object.keys(components) + .filter(componentName => { + const component = components[componentName]; + return !( + component.excludedPlatforms && + component.excludedPlatforms.includes('iOS') + ); + }) + .map(componentName => { + const replacedTemplate = LookupMapTemplate({ + className: componentName, + libraryName, + }); + return replacedTemplate; + }); + return componentTemplates.length > 0 ? componentTemplates : null; + }) + .filter(Boolean) + .join('\n'); + return generateSupportedApplePlatformsMacro( + generatedLookup, + librarySupportedApplePlatforms, + ); + }) + .join('\n'); + const replacedTemplate = FileTemplate({ + lookupMap, + }); + return new Map([[fileName, replacedTemplate]]); + }, +}; diff --git a/packages/react-native-codegen/lib/generators/components/GenerateThirdPartyFabricComponentsProviderObjCpp.js.flow b/packages/react-native-codegen/lib/generators/components/GenerateThirdPartyFabricComponentsProviderObjCpp.js.flow new file mode 100644 index 00000000000000..ad99c964437eaa --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/GenerateThirdPartyFabricComponentsProviderObjCpp.js.flow @@ -0,0 +1,125 @@ +/** + * 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 {SchemaType} from '../../CodegenSchema'; + +const { + generateSupportedApplePlatformsMacro, +} = require('./ComponentsProviderUtils'); + +// File path -> contents +type FilesOutput = Map; + +const FileTemplate = ({lookupMap}: {lookupMap: 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 GenerateRCTThirdPartyFabricComponentsProviderCpp + */ + +// OSS-compatibility layer + +#import "RCTThirdPartyFabricComponentsProvider.h" + +#import +#import + +Class RCTThirdPartyFabricComponentsProvider(const char *name) { + static std::unordered_map sFabricComponentsClassMap = { + #if RCT_NEW_ARCH_ENABLED + #ifndef RCT_DYNAMIC_FRAMEWORKS +${lookupMap} + #endif + #endif + }; + + auto p = sFabricComponentsClassMap.find(name); + if (p != sFabricComponentsClassMap.end()) { + auto classFunc = p->second; + return classFunc(); + } + return nil; +} +`; + +const LookupMapTemplate = ({ + className, + libraryName, +}: { + className: string, + libraryName: string, +}) => ` + {"${className}", ${className}Cls}, // ${libraryName}`; + +module.exports = { + generate( + schemas: {[string]: SchemaType}, + supportedApplePlatforms?: {[string]: {[string]: boolean}}, + ): FilesOutput { + const fileName = 'RCTThirdPartyFabricComponentsProvider.mm'; + + const lookupMap = Object.keys(schemas) + .map(libraryName => { + const schema = schemas[libraryName]; + const librarySupportedApplePlatforms = + supportedApplePlatforms?.[libraryName]; + + const generatedLookup = 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; + } + + const componentTemplates = Object.keys(components) + .filter(componentName => { + const component = components[componentName]; + return !( + component.excludedPlatforms && + component.excludedPlatforms.includes('iOS') + ); + }) + .map(componentName => { + const replacedTemplate = LookupMapTemplate({ + className: componentName, + libraryName, + }); + + return replacedTemplate; + }); + + return componentTemplates.length > 0 ? componentTemplates : null; + }) + .filter(Boolean) + .join('\n'); + + return generateSupportedApplePlatformsMacro( + generatedLookup, + librarySupportedApplePlatforms, + ); + }) + .join('\n'); + + const replacedTemplate = FileTemplate({lookupMap}); + + return new Map([[fileName, replacedTemplate]]); + }, +}; diff --git a/packages/react-native-codegen/lib/generators/components/GenerateViewConfigJs.js b/packages/react-native-codegen/lib/generators/components/GenerateViewConfigJs.js new file mode 100644 index 00000000000000..81a2b604509009 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/GenerateViewConfigJs.js @@ -0,0 +1,414 @@ +/** + * 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 j = require('jscodeshift'); + +// File path -> contents + +const FileTemplate = ({imports, componentConfig}) => ` +/** + * 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. + * + * @flow + * + * ${'@'}generated by codegen project: GenerateViewConfigJs.js + */ + +'use strict'; + +${imports} + +${componentConfig} +`; + +// We use this to add to a set. Need to make sure we aren't importing +// this multiple times. +const UIMANAGER_IMPORT = 'const {UIManager} = require("react-native")'; +function getReactDiffProcessValue(typeAnnotation) { + switch (typeAnnotation.type) { + case 'BooleanTypeAnnotation': + case 'StringTypeAnnotation': + case 'Int32TypeAnnotation': + case 'DoubleTypeAnnotation': + case 'FloatTypeAnnotation': + case 'ObjectTypeAnnotation': + case 'StringEnumTypeAnnotation': + case 'Int32EnumTypeAnnotation': + case 'MixedTypeAnnotation': + return j.literal(true); + case 'ReservedPropTypeAnnotation': + switch (typeAnnotation.name) { + case 'ColorPrimitive': + return j.template + .expression`{ process: require('react-native/Libraries/StyleSheet/processColor').default }`; + case 'ImageSourcePrimitive': + return j.template + .expression`{ process: require('react-native/Libraries/Image/resolveAssetSource') }`; + case 'ImageRequestPrimitive': + throw new Error('ImageRequest should not be used in props'); + case 'PointPrimitive': + return j.template + .expression`{ diff: require('react-native/Libraries/Utilities/differ/pointsDiffer') }`; + case 'EdgeInsetsPrimitive': + return j.template + .expression`{ diff: require('react-native/Libraries/Utilities/differ/insetsDiffer') }`; + case 'DimensionPrimitive': + return j.literal(true); + default: + typeAnnotation.name; + throw new Error( + `Received unknown native typeAnnotation: "${typeAnnotation.name}"`, + ); + } + case 'ArrayTypeAnnotation': + if (typeAnnotation.elementType.type === 'ReservedPropTypeAnnotation') { + switch (typeAnnotation.elementType.name) { + case 'ColorPrimitive': + return j.template + .expression`{ process: require('react-native/Libraries/StyleSheet/processColorArray') }`; + case 'ImageSourcePrimitive': + case 'PointPrimitive': + case 'EdgeInsetsPrimitive': + case 'DimensionPrimitive': + return j.literal(true); + default: + throw new Error( + `Received unknown array native typeAnnotation: "${typeAnnotation.elementType.name}"`, + ); + } + } + return j.literal(true); + default: + typeAnnotation; + throw new Error( + `Received unknown typeAnnotation: "${typeAnnotation.type}"`, + ); + } +} +const ComponentTemplate = ({ + componentName, + paperComponentName, + paperComponentNameDeprecated, +}) => { + const nativeComponentName = + paperComponentName !== null && paperComponentName !== void 0 + ? paperComponentName + : componentName; + return ` +let nativeComponentName = '${nativeComponentName}'; +${ + paperComponentNameDeprecated != null + ? DeprecatedComponentNameCheckTemplate({ + componentName, + paperComponentNameDeprecated, + }) + : '' +} + +export const __INTERNAL_VIEW_CONFIG = VIEW_CONFIG; + +export default NativeComponentRegistry.get(nativeComponentName, () => __INTERNAL_VIEW_CONFIG); +`.trim(); +}; + +// Check whether the native component exists in the app binary. +// Old getViewManagerConfig() checks for the existance of the native Paper view manager. Not available in Bridgeless. +// New hasViewManagerConfig() queries Fabric’s native component registry directly. +const DeprecatedComponentNameCheckTemplate = ({ + componentName, + paperComponentNameDeprecated, +}) => + ` +if (UIManager.hasViewManagerConfig('${componentName}')) { + nativeComponentName = '${componentName}'; +} else if (UIManager.hasViewManagerConfig('${paperComponentNameDeprecated}')) { + nativeComponentName = '${paperComponentNameDeprecated}'; +} else { + throw new Error('Failed to find native component for either "${componentName}" or "${paperComponentNameDeprecated}"'); +} +`.trim(); + +// Replicates the behavior of RCTNormalizeInputEventName in RCTEventDispatcher.m +function normalizeInputEventName(name) { + if (name.startsWith('on')) { + return name.replace(/^on/, 'top'); + } else if (!name.startsWith('top')) { + return `top${name[0].toUpperCase()}${name.slice(1)}`; + } + return name; +} + +// Replicates the behavior of viewConfig in RCTComponentData.m +function getValidAttributesForEvents(events, imports) { + imports.add( + "const {ConditionallyIgnoredEventHandlers} = require('react-native/Libraries/NativeComponent/ViewConfigIgnore');", + ); + const validAttributes = j.objectExpression( + events.map(eventType => { + return j.property('init', j.identifier(eventType.name), j.literal(true)); + }), + ); + return j.callExpression(j.identifier('ConditionallyIgnoredEventHandlers'), [ + validAttributes, + ]); +} +function generateBubblingEventInfo(event, nameOveride) { + return j.property( + 'init', + j.identifier(normalizeInputEventName(nameOveride || event.name)), + j.objectExpression([ + j.property( + 'init', + j.identifier('phasedRegistrationNames'), + j.objectExpression([ + j.property( + 'init', + j.identifier('captured'), + j.literal(`${event.name}Capture`), + ), + j.property('init', j.identifier('bubbled'), j.literal(event.name)), + ]), + ), + ]), + ); +} +function generateDirectEventInfo(event, nameOveride) { + return j.property( + 'init', + j.identifier(normalizeInputEventName(nameOveride || event.name)), + j.objectExpression([ + j.property( + 'init', + j.identifier('registrationName'), + j.literal(event.name), + ), + ]), + ); +} +function buildViewConfig(schema, componentName, component, imports) { + const componentProps = component.props; + const componentEvents = component.events; + component.extendsProps.forEach(extendProps => { + switch (extendProps.type) { + case 'ReactNativeBuiltInType': + switch (extendProps.knownTypeName) { + case 'ReactNativeCoreViewProps': + imports.add( + "const NativeComponentRegistry = require('react-native/Libraries/NativeComponent/NativeComponentRegistry');", + ); + return; + default: + extendProps.knownTypeName; + throw new Error('Invalid knownTypeName'); + } + default: + extendProps.type; + throw new Error('Invalid extended type'); + } + }); + const validAttributes = j.objectExpression([ + ...componentProps.map(schemaProp => { + return j.property( + 'init', + j.identifier(schemaProp.name), + getReactDiffProcessValue(schemaProp.typeAnnotation), + ); + }), + ...(componentEvents.length > 0 + ? [ + j.spreadProperty( + getValidAttributesForEvents(componentEvents, imports), + ), + ] + : []), + ]); + const bubblingEventNames = component.events + .filter(event => event.bubblingType === 'bubble') + .reduce((bubblingEvents, event) => { + // We add in the deprecated paper name so that it is in the view config. + // This means either the old event name or the new event name can fire + // and be sent to the listener until the old top level name is removed. + if (event.paperTopLevelNameDeprecated) { + bubblingEvents.push( + generateBubblingEventInfo(event, event.paperTopLevelNameDeprecated), + ); + } else { + bubblingEvents.push(generateBubblingEventInfo(event)); + } + return bubblingEvents; + }, []); + const bubblingEvents = + bubblingEventNames.length > 0 + ? j.property( + 'init', + j.identifier('bubblingEventTypes'), + j.objectExpression(bubblingEventNames), + ) + : null; + const directEventNames = component.events + .filter(event => event.bubblingType === 'direct') + .reduce((directEvents, event) => { + // We add in the deprecated paper name so that it is in the view config. + // This means either the old event name or the new event name can fire + // and be sent to the listener until the old top level name is removed. + if (event.paperTopLevelNameDeprecated) { + directEvents.push( + generateDirectEventInfo(event, event.paperTopLevelNameDeprecated), + ); + } else { + directEvents.push(generateDirectEventInfo(event)); + } + return directEvents; + }, []); + const directEvents = + directEventNames.length > 0 + ? j.property( + 'init', + j.identifier('directEventTypes'), + j.objectExpression(directEventNames), + ) + : null; + const properties = [ + j.property( + 'init', + j.identifier('uiViewClassName'), + j.literal(componentName), + ), + bubblingEvents, + directEvents, + j.property('init', j.identifier('validAttributes'), validAttributes), + ].filter(Boolean); + return j.objectExpression(properties); +} +function buildCommands(schema, componentName, component, imports) { + const commands = component.commands; + if (commands.length === 0) { + return null; + } + imports.add( + 'const {dispatchCommand} = require("react-native/Libraries/ReactNative/RendererProxy");', + ); + const properties = commands.map(command => { + const commandName = command.name; + const params = command.typeAnnotation.params; + const commandNameLiteral = j.literal(commandName); + const commandNameIdentifier = j.identifier(commandName); + const arrayParams = j.arrayExpression( + params.map(param => { + return j.identifier(param.name); + }), + ); + const expression = j.template + .expression`dispatchCommand(ref, ${commandNameLiteral}, ${arrayParams})`; + const functionParams = params.map(param => { + return j.identifier(param.name); + }); + const property = j.property( + 'init', + commandNameIdentifier, + j.functionExpression( + null, + [j.identifier('ref'), ...functionParams], + j.blockStatement([j.expressionStatement(expression)]), + ), + ); + property.method = true; + return property; + }); + return j.exportNamedDeclaration( + j.variableDeclaration('const', [ + j.variableDeclarator( + j.identifier('Commands'), + j.objectExpression(properties), + ), + ]), + ); +} +module.exports = { + generate(libraryName, schema) { + try { + const fileName = `${libraryName}NativeViewConfig.js`; + const imports = new Set(); + const moduleResults = Object.keys(schema.modules) + .map(moduleName => { + const module = schema.modules[moduleName]; + if (module.type !== 'Component') { + return; + } + const components = module.components; + return Object.keys(components) + .map(componentName => { + var _component$paperCompo; + const component = components[componentName]; + if (component.paperComponentNameDeprecated) { + imports.add(UIMANAGER_IMPORT); + } + const replacedTemplate = ComponentTemplate({ + componentName, + paperComponentName: component.paperComponentName, + paperComponentNameDeprecated: + component.paperComponentNameDeprecated, + }); + const replacedSourceRoot = j.withParser('flow')(replacedTemplate); + const paperComponentName = + (_component$paperCompo = component.paperComponentName) !== + null && _component$paperCompo !== void 0 + ? _component$paperCompo + : componentName; + replacedSourceRoot + .find(j.Identifier, { + name: 'VIEW_CONFIG', + }) + .replaceWith( + buildViewConfig( + schema, + paperComponentName, + component, + imports, + ), + ); + const commands = buildCommands( + schema, + paperComponentName, + component, + imports, + ); + if (commands) { + replacedSourceRoot + .find(j.ExportDefaultDeclaration) + .insertAfter(j(commands).toSource()); + } + const replacedSource = replacedSourceRoot.toSource({ + quote: 'single', + trailingComma: true, + }); + return replacedSource; + }) + .join('\n\n'); + }) + .filter(Boolean) + .join('\n\n'); + const replacedTemplate = FileTemplate({ + componentConfig: moduleResults, + imports: Array.from(imports).sort().join('\n'), + }); + return new Map([[fileName, replacedTemplate]]); + } catch (error) { + console.error(`\nError parsing schema for ${libraryName}\n`); + console.error(JSON.stringify(schema)); + throw error; + } + }, +}; diff --git a/packages/react-native-codegen/lib/generators/components/GenerateViewConfigJs.js.flow b/packages/react-native-codegen/lib/generators/components/GenerateViewConfigJs.js.flow new file mode 100644 index 00000000000000..f09b38c7885f52 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/GenerateViewConfigJs.js.flow @@ -0,0 +1,488 @@ +/** + * 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 + * @format + */ + +'use strict'; +import type { + ComponentShape, + EventTypeShape, + PropTypeAnnotation, +} from '../../CodegenSchema'; +import type {SchemaType} from '../../CodegenSchema'; + +const j = require('jscodeshift'); + +// File path -> contents +type FilesOutput = Map; + +const FileTemplate = ({ + imports, + componentConfig, +}: { + imports: string, + componentConfig: 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. + * + * @flow + * + * ${'@'}generated by codegen project: GenerateViewConfigJs.js + */ + +'use strict'; + +${imports} + +${componentConfig} +`; + +// We use this to add to a set. Need to make sure we aren't importing +// this multiple times. +const UIMANAGER_IMPORT = 'const {UIManager} = require("react-native")'; + +function getReactDiffProcessValue(typeAnnotation: PropTypeAnnotation) { + switch (typeAnnotation.type) { + case 'BooleanTypeAnnotation': + case 'StringTypeAnnotation': + case 'Int32TypeAnnotation': + case 'DoubleTypeAnnotation': + case 'FloatTypeAnnotation': + case 'ObjectTypeAnnotation': + case 'StringEnumTypeAnnotation': + case 'Int32EnumTypeAnnotation': + case 'MixedTypeAnnotation': + return j.literal(true); + case 'ReservedPropTypeAnnotation': + switch (typeAnnotation.name) { + case 'ColorPrimitive': + return j.template + .expression`{ process: require('react-native/Libraries/StyleSheet/processColor').default }`; + case 'ImageSourcePrimitive': + return j.template + .expression`{ process: require('react-native/Libraries/Image/resolveAssetSource') }`; + case 'ImageRequestPrimitive': + throw new Error('ImageRequest should not be used in props'); + case 'PointPrimitive': + return j.template + .expression`{ diff: require('react-native/Libraries/Utilities/differ/pointsDiffer') }`; + case 'EdgeInsetsPrimitive': + return j.template + .expression`{ diff: require('react-native/Libraries/Utilities/differ/insetsDiffer') }`; + case 'DimensionPrimitive': + return j.literal(true); + default: + (typeAnnotation.name: empty); + throw new Error( + `Received unknown native typeAnnotation: "${typeAnnotation.name}"`, + ); + } + case 'ArrayTypeAnnotation': + if (typeAnnotation.elementType.type === 'ReservedPropTypeAnnotation') { + switch (typeAnnotation.elementType.name) { + case 'ColorPrimitive': + return j.template + .expression`{ process: require('react-native/Libraries/StyleSheet/processColorArray') }`; + case 'ImageSourcePrimitive': + case 'PointPrimitive': + case 'EdgeInsetsPrimitive': + case 'DimensionPrimitive': + return j.literal(true); + default: + throw new Error( + `Received unknown array native typeAnnotation: "${typeAnnotation.elementType.name}"`, + ); + } + } + return j.literal(true); + default: + (typeAnnotation: empty); + throw new Error( + `Received unknown typeAnnotation: "${typeAnnotation.type}"`, + ); + } +} + +const ComponentTemplate = ({ + componentName, + paperComponentName, + paperComponentNameDeprecated, +}: { + componentName: string, + paperComponentName: ?string, + paperComponentNameDeprecated: ?string, +}) => { + const nativeComponentName = paperComponentName ?? componentName; + + return ` +let nativeComponentName = '${nativeComponentName}'; +${ + paperComponentNameDeprecated != null + ? DeprecatedComponentNameCheckTemplate({ + componentName, + paperComponentNameDeprecated, + }) + : '' +} + +export const __INTERNAL_VIEW_CONFIG = VIEW_CONFIG; + +export default NativeComponentRegistry.get(nativeComponentName, () => __INTERNAL_VIEW_CONFIG); +`.trim(); +}; + +// Check whether the native component exists in the app binary. +// Old getViewManagerConfig() checks for the existance of the native Paper view manager. Not available in Bridgeless. +// New hasViewManagerConfig() queries Fabric’s native component registry directly. +const DeprecatedComponentNameCheckTemplate = ({ + componentName, + paperComponentNameDeprecated, +}: { + componentName: string, + paperComponentNameDeprecated: string, +}) => + ` +if (UIManager.hasViewManagerConfig('${componentName}')) { + nativeComponentName = '${componentName}'; +} else if (UIManager.hasViewManagerConfig('${paperComponentNameDeprecated}')) { + nativeComponentName = '${paperComponentNameDeprecated}'; +} else { + throw new Error('Failed to find native component for either "${componentName}" or "${paperComponentNameDeprecated}"'); +} +`.trim(); + +// Replicates the behavior of RCTNormalizeInputEventName in RCTEventDispatcher.m +function normalizeInputEventName(name: string) { + if (name.startsWith('on')) { + return name.replace(/^on/, 'top'); + } else if (!name.startsWith('top')) { + return `top${name[0].toUpperCase()}${name.slice(1)}`; + } + + return name; +} + +// Replicates the behavior of viewConfig in RCTComponentData.m +function getValidAttributesForEvents( + events: $ReadOnlyArray, + imports: Set, +) { + imports.add( + "const {ConditionallyIgnoredEventHandlers} = require('react-native/Libraries/NativeComponent/ViewConfigIgnore');", + ); + + const validAttributes = j.objectExpression( + events.map(eventType => { + return j.property('init', j.identifier(eventType.name), j.literal(true)); + }), + ); + + return j.callExpression(j.identifier('ConditionallyIgnoredEventHandlers'), [ + validAttributes, + ]); +} + +function generateBubblingEventInfo( + event: EventTypeShape, + nameOveride: void | string, +) { + return j.property( + 'init', + j.identifier(normalizeInputEventName(nameOveride || event.name)), + j.objectExpression([ + j.property( + 'init', + j.identifier('phasedRegistrationNames'), + j.objectExpression([ + j.property( + 'init', + j.identifier('captured'), + j.literal(`${event.name}Capture`), + ), + j.property('init', j.identifier('bubbled'), j.literal(event.name)), + ]), + ), + ]), + ); +} + +function generateDirectEventInfo( + event: EventTypeShape, + nameOveride: void | string, +) { + return j.property( + 'init', + j.identifier(normalizeInputEventName(nameOveride || event.name)), + j.objectExpression([ + j.property( + 'init', + j.identifier('registrationName'), + j.literal(event.name), + ), + ]), + ); +} + +function buildViewConfig( + schema: SchemaType, + componentName: string, + component: ComponentShape, + imports: Set, +) { + const componentProps = component.props; + const componentEvents = component.events; + + component.extendsProps.forEach(extendProps => { + switch (extendProps.type) { + case 'ReactNativeBuiltInType': + switch (extendProps.knownTypeName) { + case 'ReactNativeCoreViewProps': + imports.add( + "const NativeComponentRegistry = require('react-native/Libraries/NativeComponent/NativeComponentRegistry');", + ); + + return; + default: + (extendProps.knownTypeName: empty); + throw new Error('Invalid knownTypeName'); + } + default: + (extendProps.type: empty); + throw new Error('Invalid extended type'); + } + }); + + const validAttributes = j.objectExpression([ + ...componentProps.map(schemaProp => { + return j.property( + 'init', + j.identifier(schemaProp.name), + getReactDiffProcessValue(schemaProp.typeAnnotation), + ); + }), + ...(componentEvents.length > 0 + ? [ + j.spreadProperty( + getValidAttributesForEvents(componentEvents, imports), + ), + ] + : []), + ]); + + const bubblingEventNames = component.events + .filter(event => event.bubblingType === 'bubble') + .reduce((bubblingEvents: Array, event) => { + // We add in the deprecated paper name so that it is in the view config. + // This means either the old event name or the new event name can fire + // and be sent to the listener until the old top level name is removed. + if (event.paperTopLevelNameDeprecated) { + bubblingEvents.push( + generateBubblingEventInfo(event, event.paperTopLevelNameDeprecated), + ); + } else { + bubblingEvents.push(generateBubblingEventInfo(event)); + } + return bubblingEvents; + }, []); + + const bubblingEvents = + bubblingEventNames.length > 0 + ? j.property( + 'init', + j.identifier('bubblingEventTypes'), + j.objectExpression(bubblingEventNames), + ) + : null; + + const directEventNames = component.events + .filter(event => event.bubblingType === 'direct') + .reduce((directEvents: Array, event) => { + // We add in the deprecated paper name so that it is in the view config. + // This means either the old event name or the new event name can fire + // and be sent to the listener until the old top level name is removed. + if (event.paperTopLevelNameDeprecated) { + directEvents.push( + generateDirectEventInfo(event, event.paperTopLevelNameDeprecated), + ); + } else { + directEvents.push(generateDirectEventInfo(event)); + } + return directEvents; + }, []); + + const directEvents = + directEventNames.length > 0 + ? j.property( + 'init', + j.identifier('directEventTypes'), + j.objectExpression(directEventNames), + ) + : null; + + const properties = [ + j.property( + 'init', + j.identifier('uiViewClassName'), + j.literal(componentName), + ), + bubblingEvents, + directEvents, + j.property('init', j.identifier('validAttributes'), validAttributes), + ].filter(Boolean); + + return j.objectExpression(properties); +} + +function buildCommands( + schema: SchemaType, + componentName: string, + component: ComponentShape, + imports: Set, +) { + const commands = component.commands; + + if (commands.length === 0) { + return null; + } + + imports.add( + 'const {dispatchCommand} = require("react-native/Libraries/ReactNative/RendererProxy");', + ); + + const properties = commands.map(command => { + const commandName = command.name; + const params = command.typeAnnotation.params; + + const commandNameLiteral = j.literal(commandName); + const commandNameIdentifier = j.identifier(commandName); + const arrayParams = j.arrayExpression( + params.map(param => { + return j.identifier(param.name); + }), + ); + + const expression = j.template + .expression`dispatchCommand(ref, ${commandNameLiteral}, ${arrayParams})`; + + const functionParams = params.map(param => { + return j.identifier(param.name); + }); + + const property = j.property( + 'init', + commandNameIdentifier, + j.functionExpression( + null, + [j.identifier('ref'), ...functionParams], + j.blockStatement([j.expressionStatement(expression)]), + ), + ); + property.method = true; + + return property; + }); + + return j.exportNamedDeclaration( + j.variableDeclaration('const', [ + j.variableDeclarator( + j.identifier('Commands'), + j.objectExpression(properties), + ), + ]), + ); +} + +module.exports = { + generate(libraryName: string, schema: SchemaType): FilesOutput { + try { + const fileName = `${libraryName}NativeViewConfig.js`; + const imports: Set = new Set(); + + const moduleResults = Object.keys(schema.modules) + .map(moduleName => { + const module = schema.modules[moduleName]; + if (module.type !== 'Component') { + return; + } + + const {components} = module; + + return Object.keys(components) + .map((componentName: string) => { + const component = components[componentName]; + + if (component.paperComponentNameDeprecated) { + imports.add(UIMANAGER_IMPORT); + } + + const replacedTemplate = ComponentTemplate({ + componentName, + paperComponentName: component.paperComponentName, + paperComponentNameDeprecated: + component.paperComponentNameDeprecated, + }); + + const replacedSourceRoot = j.withParser('flow')(replacedTemplate); + + const paperComponentName = + component.paperComponentName ?? componentName; + + replacedSourceRoot + .find(j.Identifier, { + name: 'VIEW_CONFIG', + }) + .replaceWith( + buildViewConfig( + schema, + paperComponentName, + component, + imports, + ), + ); + + const commands = buildCommands( + schema, + paperComponentName, + component, + imports, + ); + if (commands) { + replacedSourceRoot + .find(j.ExportDefaultDeclaration) + .insertAfter(j(commands).toSource()); + } + + const replacedSource: string = replacedSourceRoot.toSource({ + quote: 'single', + trailingComma: true, + }); + + return replacedSource; + }) + .join('\n\n'); + }) + .filter(Boolean) + .join('\n\n'); + + const replacedTemplate = FileTemplate({ + componentConfig: moduleResults, + imports: Array.from(imports).sort().join('\n'), + }); + + return new Map([[fileName, replacedTemplate]]); + } catch (error) { + console.error(`\nError parsing schema for ${libraryName}\n`); + console.error(JSON.stringify(schema)); + throw error; + } + }, +}; diff --git a/packages/react-native-codegen/lib/generators/components/JavaHelpers.js b/packages/react-native-codegen/lib/generators/components/JavaHelpers.js new file mode 100644 index 00000000000000..61526fc1801cfc --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/JavaHelpers.js @@ -0,0 +1,111 @@ +/** + * 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'; + +function upperCaseFirst(inString) { + return inString[0].toUpperCase() + inString.slice(1); +} +function getInterfaceJavaClassName(componentName) { + return `${componentName.replace(/^RCT/, '')}ManagerInterface`; +} +function getDelegateJavaClassName(componentName) { + return `${componentName.replace(/^RCT/, '')}ManagerDelegate`; +} +function toSafeJavaString(input, shouldUpperCaseFirst) { + const parts = input.split('-'); + if (shouldUpperCaseFirst === false) { + return parts.join(''); + } + return parts.map(upperCaseFirst).join(''); +} +function getImports(component, type) { + const imports = new Set(); + component.extendsProps.forEach(extendProps => { + switch (extendProps.type) { + case 'ReactNativeBuiltInType': + switch (extendProps.knownTypeName) { + case 'ReactNativeCoreViewProps': + imports.add('import android.view.View;'); + return; + default: + extendProps.knownTypeName; + throw new Error('Invalid knownTypeName'); + } + default: + extendProps.type; + throw new Error('Invalid extended type'); + } + }); + function addImportsForNativeName(name) { + switch (name) { + case 'ColorPrimitive': + if (type === 'delegate') { + imports.add('import com.facebook.react.bridge.ColorPropConverter;'); + } + return; + case 'ImageSourcePrimitive': + imports.add('import com.facebook.react.bridge.ReadableMap;'); + return; + case 'PointPrimitive': + imports.add('import com.facebook.react.bridge.ReadableMap;'); + return; + case 'EdgeInsetsPrimitive': + imports.add('import com.facebook.react.bridge.ReadableMap;'); + return; + case 'DimensionPrimitive': + if (type === 'delegate') { + imports.add( + 'import com.facebook.react.bridge.DimensionPropConverter;', + ); + } else { + imports.add('import com.facebook.yoga.YogaValue;'); + } + return; + default: + name; + throw new Error(`Invalid ReservedPropTypeAnnotation name, got ${name}`); + } + } + component.props.forEach(prop => { + const typeAnnotation = prop.typeAnnotation; + if (typeAnnotation.type === 'ReservedPropTypeAnnotation') { + addImportsForNativeName(typeAnnotation.name); + } + if (typeAnnotation.type === 'ArrayTypeAnnotation') { + imports.add('import com.facebook.react.bridge.ReadableArray;'); + } + if (typeAnnotation.type === 'ObjectTypeAnnotation') { + imports.add('import com.facebook.react.bridge.ReadableMap;'); + } + if (typeAnnotation.type === 'MixedTypeAnnotation') { + if (type === 'delegate') { + imports.add('import com.facebook.react.bridge.DynamicFromObject;'); + } else { + imports.add('import com.facebook.react.bridge.Dynamic;'); + } + } + }); + component.commands.forEach(command => { + command.typeAnnotation.params.forEach(param => { + const cmdParamType = param.typeAnnotation.type; + if (cmdParamType === 'ArrayTypeAnnotation') { + imports.add('import com.facebook.react.bridge.ReadableArray;'); + } + }); + }); + return imports; +} +module.exports = { + getInterfaceJavaClassName, + getDelegateJavaClassName, + toSafeJavaString, + getImports, +}; diff --git a/packages/react-native-codegen/lib/generators/components/JavaHelpers.js.flow b/packages/react-native-codegen/lib/generators/components/JavaHelpers.js.flow new file mode 100644 index 00000000000000..9b835bb6f91820 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/JavaHelpers.js.flow @@ -0,0 +1,147 @@ +/** + * 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'; + +function upperCaseFirst(inString: string): string { + return inString[0].toUpperCase() + inString.slice(1); +} + +function getInterfaceJavaClassName(componentName: string): string { + return `${componentName.replace(/^RCT/, '')}ManagerInterface`; +} + +function getDelegateJavaClassName(componentName: string): string { + return `${componentName.replace(/^RCT/, '')}ManagerDelegate`; +} + +function toSafeJavaString( + input: string, + shouldUpperCaseFirst?: boolean, +): string { + const parts = input.split('-'); + + if (shouldUpperCaseFirst === false) { + return parts.join(''); + } + + return parts.map(upperCaseFirst).join(''); +} + +function getImports( + component: ComponentShape, + type: 'interface' | 'delegate', +): Set { + const imports: Set = new Set(); + + component.extendsProps.forEach(extendProps => { + switch (extendProps.type) { + case 'ReactNativeBuiltInType': + switch (extendProps.knownTypeName) { + case 'ReactNativeCoreViewProps': + imports.add('import android.view.View;'); + return; + default: + (extendProps.knownTypeName: empty); + throw new Error('Invalid knownTypeName'); + } + default: + (extendProps.type: empty); + throw new Error('Invalid extended type'); + } + }); + + function addImportsForNativeName( + name: + | 'ColorPrimitive' + | 'EdgeInsetsPrimitive' + | 'ImageSourcePrimitive' + | 'PointPrimitive' + | 'DimensionPrimitive' + | $TEMPORARY$string<'ColorPrimitive'> + | $TEMPORARY$string<'EdgeInsetsPrimitive'> + | $TEMPORARY$string<'ImageSourcePrimitive'> + | $TEMPORARY$string<'PointPrimitive'> + | $TEMPORARY$string<'DimensionPrimitive'>, + ) { + switch (name) { + case 'ColorPrimitive': + if (type === 'delegate') { + imports.add('import com.facebook.react.bridge.ColorPropConverter;'); + } + return; + case 'ImageSourcePrimitive': + imports.add('import com.facebook.react.bridge.ReadableMap;'); + return; + case 'PointPrimitive': + imports.add('import com.facebook.react.bridge.ReadableMap;'); + return; + case 'EdgeInsetsPrimitive': + imports.add('import com.facebook.react.bridge.ReadableMap;'); + return; + case 'DimensionPrimitive': + if (type === 'delegate') { + imports.add( + 'import com.facebook.react.bridge.DimensionPropConverter;', + ); + } else { + imports.add('import com.facebook.yoga.YogaValue;'); + } + return; + default: + (name: empty); + throw new Error(`Invalid ReservedPropTypeAnnotation name, got ${name}`); + } + } + + component.props.forEach(prop => { + const typeAnnotation = prop.typeAnnotation; + + if (typeAnnotation.type === 'ReservedPropTypeAnnotation') { + addImportsForNativeName(typeAnnotation.name); + } + + if (typeAnnotation.type === 'ArrayTypeAnnotation') { + imports.add('import com.facebook.react.bridge.ReadableArray;'); + } + + if (typeAnnotation.type === 'ObjectTypeAnnotation') { + imports.add('import com.facebook.react.bridge.ReadableMap;'); + } + + if (typeAnnotation.type === 'MixedTypeAnnotation') { + if (type === 'delegate') { + imports.add('import com.facebook.react.bridge.DynamicFromObject;'); + } else { + imports.add('import com.facebook.react.bridge.Dynamic;'); + } + } + }); + + component.commands.forEach(command => { + command.typeAnnotation.params.forEach(param => { + const cmdParamType = param.typeAnnotation.type; + if (cmdParamType === 'ArrayTypeAnnotation') { + imports.add('import com.facebook.react.bridge.ReadableArray;'); + } + }); + }); + + return imports; +} + +module.exports = { + getInterfaceJavaClassName, + getDelegateJavaClassName, + toSafeJavaString, + getImports, +}; diff --git a/packages/react-native-codegen/lib/generators/components/__test_fixtures__/fixtures.js b/packages/react-native-codegen/lib/generators/components/__test_fixtures__/fixtures.js new file mode 100644 index 00000000000000..e61cd063b453d4 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/__test_fixtures__/fixtures.js @@ -0,0 +1,1878 @@ +/** + * 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 NO_PROPS_NO_EVENTS = { + modules: { + NoPropsNoEvents: { + type: 'Component', + components: { + NoPropsNoEventsComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [], + commands: [], + }, + }, + }, + }, +}; +const INTERFACE_ONLY = { + modules: { + Switch: { + type: 'Component', + components: { + InterfaceOnlyComponent: { + interfaceOnly: true, + paperComponentName: 'RCTInterfaceOnlyComponent', + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [ + { + name: 'onChange', + optional: true, + bubblingType: 'bubble', + typeAnnotation: { + type: 'EventTypeAnnotation', + argument: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'value', + optional: false, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + ], + }, + }, + }, + ], + props: [ + { + name: 'accessibilityHint', + optional: true, + typeAnnotation: { + type: 'StringTypeAnnotation', + default: '', + }, + }, + ], + commands: [], + }, + }, + }, + }, +}; +const EVENTS_WITH_PAPER_NAME = { + modules: { + Switch: { + type: 'Component', + components: { + InterfaceOnlyComponent: { + interfaceOnly: true, + paperComponentName: 'RCTInterfaceOnlyComponent', + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [ + { + name: 'onChange', + paperTopLevelNameDeprecated: 'paperChange', + optional: true, + bubblingType: 'bubble', + typeAnnotation: { + type: 'EventTypeAnnotation', + argument: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'value', + optional: false, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + ], + }, + }, + }, + { + name: 'onDirectChange', + paperTopLevelNameDeprecated: 'paperDirectChange', + optional: true, + bubblingType: 'direct', + typeAnnotation: { + type: 'EventTypeAnnotation', + argument: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'value', + optional: false, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + ], + }, + }, + }, + ], + props: [], + commands: [], + }, + }, + }, + }, +}; +const BOOLEAN_PROP = { + modules: { + Switch: { + type: 'Component', + components: { + BooleanPropNativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'disabled', + optional: true, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + default: false, + }, + }, + ], + commands: [], + }, + }, + }, + }, +}; +const STRING_PROP = { + modules: { + Switch: { + type: 'Component', + components: { + StringPropComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'accessibilityHint', + optional: true, + typeAnnotation: { + type: 'StringTypeAnnotation', + default: '', + }, + }, + { + name: 'accessibilityRole', + optional: true, + typeAnnotation: { + type: 'StringTypeAnnotation', + default: null, + }, + }, + ], + commands: [], + }, + }, + }, + }, +}; +const INTEGER_PROPS = { + modules: { + Switch: { + type: 'Component', + components: { + IntegerPropNativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'progress1', + optional: true, + typeAnnotation: { + type: 'Int32TypeAnnotation', + default: 0, + }, + }, + { + name: 'progress2', + optional: true, + typeAnnotation: { + type: 'Int32TypeAnnotation', + default: -1, + }, + }, + { + name: 'progress3', + optional: true, + typeAnnotation: { + type: 'Int32TypeAnnotation', + default: 10, + }, + }, + ], + commands: [], + }, + }, + }, + }, +}; +const FLOAT_PROPS = { + modules: { + Switch: { + type: 'Component', + components: { + FloatPropNativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'blurRadius', + optional: false, + typeAnnotation: { + type: 'FloatTypeAnnotation', + default: 0.0, + }, + }, + { + name: 'blurRadius2', + optional: true, + typeAnnotation: { + type: 'FloatTypeAnnotation', + default: 0.001, + }, + }, + { + name: 'blurRadius3', + optional: true, + typeAnnotation: { + type: 'FloatTypeAnnotation', + default: 2.1, + }, + }, + { + name: 'blurRadius4', + optional: true, + typeAnnotation: { + type: 'FloatTypeAnnotation', + default: 0, + }, + }, + { + name: 'blurRadius5', + optional: true, + typeAnnotation: { + type: 'FloatTypeAnnotation', + default: 1, + }, + }, + { + name: 'blurRadius6', + optional: true, + typeAnnotation: { + type: 'FloatTypeAnnotation', + default: -0.0, + }, + }, + ], + commands: [], + }, + }, + }, + }, +}; +const DOUBLE_PROPS = { + modules: { + Switch: { + type: 'Component', + components: { + DoublePropNativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'blurRadius', + optional: false, + typeAnnotation: { + type: 'DoubleTypeAnnotation', + default: 0.0, + }, + }, + { + name: 'blurRadius2', + optional: true, + typeAnnotation: { + type: 'DoubleTypeAnnotation', + default: 0.001, + }, + }, + { + name: 'blurRadius3', + optional: true, + typeAnnotation: { + type: 'DoubleTypeAnnotation', + default: 2.1, + }, + }, + { + name: 'blurRadius4', + optional: true, + typeAnnotation: { + type: 'DoubleTypeAnnotation', + default: 0, + }, + }, + { + name: 'blurRadius5', + optional: true, + typeAnnotation: { + type: 'DoubleTypeAnnotation', + default: 1, + }, + }, + { + name: 'blurRadius6', + optional: true, + typeAnnotation: { + type: 'DoubleTypeAnnotation', + default: -0.0, + }, + }, + ], + commands: [], + }, + }, + }, + }, +}; +const COLOR_PROP = { + modules: { + Switch: { + type: 'Component', + components: { + ColorPropNativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'tintColor', + optional: true, + typeAnnotation: { + type: 'ReservedPropTypeAnnotation', + name: 'ColorPrimitive', + }, + }, + ], + commands: [], + }, + }, + }, + }, +}; +const IMAGE_PROP = { + modules: { + Slider: { + type: 'Component', + components: { + ImagePropNativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'thumbImage', + optional: true, + typeAnnotation: { + type: 'ReservedPropTypeAnnotation', + name: 'ImageSourcePrimitive', + }, + }, + ], + commands: [], + }, + }, + }, + }, +}; +const POINT_PROP = { + modules: { + Switch: { + type: 'Component', + components: { + PointPropNativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'startPoint', + optional: true, + typeAnnotation: { + type: 'ReservedPropTypeAnnotation', + name: 'PointPrimitive', + }, + }, + ], + commands: [], + }, + }, + }, + }, +}; +const INSETS_PROP = { + modules: { + ScrollView: { + type: 'Component', + components: { + InsetsPropNativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'contentInset', + optional: true, + typeAnnotation: { + type: 'ReservedPropTypeAnnotation', + name: 'EdgeInsetsPrimitive', + }, + }, + ], + commands: [], + }, + }, + }, + }, +}; +const DIMENSION_PROP = { + modules: { + CustomView: { + type: 'Component', + components: { + DimensionPropNativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'marginBack', + optional: true, + typeAnnotation: { + type: 'ReservedPropTypeAnnotation', + name: 'DimensionPrimitive', + }, + }, + ], + commands: [], + }, + }, + }, + }, +}; +const ARRAY_PROPS = { + modules: { + Slider: { + type: 'Component', + components: { + ArrayPropsNativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'names', + optional: true, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'StringTypeAnnotation', + }, + }, + }, + { + name: 'disableds', + optional: true, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'BooleanTypeAnnotation', + }, + }, + }, + { + name: 'progress', + optional: true, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'Int32TypeAnnotation', + }, + }, + }, + { + name: 'radii', + optional: true, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'FloatTypeAnnotation', + }, + }, + }, + { + name: 'colors', + optional: true, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'ReservedPropTypeAnnotation', + name: 'ColorPrimitive', + }, + }, + }, + { + name: 'srcs', + optional: true, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'ReservedPropTypeAnnotation', + name: 'ImageSourcePrimitive', + }, + }, + }, + { + name: 'points', + optional: true, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'ReservedPropTypeAnnotation', + name: 'PointPrimitive', + }, + }, + }, + { + name: 'dimensions', + optional: true, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'ReservedPropTypeAnnotation', + name: 'DimensionPrimitive', + }, + }, + }, + { + name: 'sizes', + optional: true, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'StringEnumTypeAnnotation', + default: 'small', + options: ['small', 'large'], + }, + }, + }, + { + name: 'object', + optional: true, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'stringProp', + optional: true, + typeAnnotation: { + type: 'StringTypeAnnotation', + default: '', + }, + }, + ], + }, + }, + }, + { + name: 'array', + optional: true, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'ObjectTypeAnnotation', + properties: [ + { + // This needs to stay the same as the object above + // to confirm that the structs are generated + // with unique non-colliding names + name: 'object', + optional: true, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'stringProp', + optional: true, + typeAnnotation: { + type: 'StringTypeAnnotation', + default: '', + }, + }, + ], + }, + }, + }, + ], + }, + }, + }, + { + name: 'arrayOfArrayOfObject', + optional: true, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'stringProp', + optional: true, + typeAnnotation: { + type: 'StringTypeAnnotation', + default: '', + }, + }, + ], + }, + }, + }, + }, + ], + commands: [], + }, + }, + }, + }, +}; +const ARRAY_PROPS_WITH_NESTED_OBJECT = { + modules: { + Slider: { + type: 'Component', + components: { + ArrayPropsNativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'nativePrimitives', + optional: true, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'colors', + optional: true, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'ReservedPropTypeAnnotation', + name: 'ColorPrimitive', + }, + }, + }, + { + name: 'srcs', + optional: true, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'ReservedPropTypeAnnotation', + name: 'ImageSourcePrimitive', + }, + }, + }, + { + name: 'points', + optional: true, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'ReservedPropTypeAnnotation', + name: 'PointPrimitive', + }, + }, + }, + ], + }, + }, + }, + ], + commands: [], + }, + }, + }, + }, +}; +const OBJECT_PROPS = { + modules: { + ObjectPropsNativeComponent: { + type: 'Component', + components: { + ObjectProps: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'objectProp', + optional: true, + typeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'stringProp', + optional: true, + typeAnnotation: { + type: 'StringTypeAnnotation', + default: '', + }, + }, + { + name: 'booleanProp', + optional: true, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + default: false, + }, + }, + { + name: 'floatProp', + optional: true, + typeAnnotation: { + type: 'FloatTypeAnnotation', + default: 0.0, + }, + }, + { + name: 'intProp', + optional: true, + typeAnnotation: { + type: 'Int32TypeAnnotation', + default: 0, + }, + }, + { + name: 'stringUserDefaultProp', + optional: true, + typeAnnotation: { + type: 'StringTypeAnnotation', + default: 'user_default', + }, + }, + { + name: 'booleanUserDefaultProp', + optional: true, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + default: true, + }, + }, + { + name: 'floatUserDefaultProp', + optional: true, + typeAnnotation: { + type: 'FloatTypeAnnotation', + default: 3.14, + }, + }, + { + name: 'intUserDefaultProp', + optional: true, + typeAnnotation: { + type: 'Int32TypeAnnotation', + default: 9999, + }, + }, + { + name: 'stringEnumProp', + optional: true, + typeAnnotation: { + type: 'StringEnumTypeAnnotation', + default: 'option1', + options: ['option1'], + }, + }, + { + name: 'intEnumProp', + optional: true, + typeAnnotation: { + type: 'Int32EnumTypeAnnotation', + default: 0, + options: [0], + }, + }, + { + name: 'objectArrayProp', + optional: false, + typeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'array', + optional: true, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'StringTypeAnnotation', + }, + }, + }, + ], + }, + }, + { + name: 'objectPrimitiveRequiredProp', + optional: false, + typeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'image', + optional: true, + typeAnnotation: { + type: 'ReservedPropTypeAnnotation', + name: 'ImageSourcePrimitive', + }, + }, + { + name: 'color', + optional: true, + typeAnnotation: { + type: 'ReservedPropTypeAnnotation', + name: 'ColorPrimitive', + }, + }, + { + name: 'point', + optional: true, + typeAnnotation: { + type: 'ReservedPropTypeAnnotation', + name: 'PointPrimitive', + }, + }, + ], + }, + }, + { + name: 'nestedPropA', + optional: false, + typeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'nestedPropB', + optional: false, + typeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'nestedPropC', + optional: true, + typeAnnotation: { + type: 'StringTypeAnnotation', + default: '', + }, + }, + ], + }, + }, + ], + }, + }, + { + name: 'nestedArrayAsProperty', + optional: false, + typeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'arrayProp', + optional: false, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'stringProp', + optional: false, + typeAnnotation: { + type: 'StringTypeAnnotation', + default: '', + }, + }, + ], + }, + }, + }, + ], + }, + }, + ], + }, + }, + ], + commands: [], + }, + }, + }, + }, +}; +const MULTI_NATIVE_PROP = { + modules: { + Slider: { + type: 'Component', + components: { + ImageColorPropNativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'thumbImage', + optional: true, + typeAnnotation: { + type: 'ReservedPropTypeAnnotation', + name: 'ImageSourcePrimitive', + }, + }, + { + name: 'color', + optional: true, + typeAnnotation: { + type: 'ReservedPropTypeAnnotation', + name: 'ColorPrimitive', + }, + }, + { + name: 'thumbTintColor', + optional: true, + typeAnnotation: { + type: 'ReservedPropTypeAnnotation', + name: 'ColorPrimitive', + }, + }, + { + name: 'point', + optional: true, + typeAnnotation: { + type: 'ReservedPropTypeAnnotation', + name: 'PointPrimitive', + }, + }, + ], + commands: [], + }, + }, + }, + }, +}; +const STRING_ENUM_PROP = { + modules: { + Switch: { + type: 'Component', + components: { + StringEnumPropsNativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'alignment', + optional: true, + typeAnnotation: { + type: 'StringEnumTypeAnnotation', + default: 'center', + options: ['top', 'center', 'bottom-right'], + }, + }, + ], + commands: [], + }, + }, + }, + }, +}; +const INT32_ENUM_PROP = { + modules: { + Switch: { + type: 'Component', + components: { + Int32EnumPropsNativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'maxInterval', + optional: true, + typeAnnotation: { + type: 'Int32EnumTypeAnnotation', + default: 0, + options: [0, 1, 2], + }, + }, + ], + commands: [], + }, + }, + }, + }, +}; +const MIXED_PROP = { + modules: { + CustomView: { + type: 'Component', + components: { + MixedPropNativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'mixedProp', + optional: false, + typeAnnotation: { + type: 'MixedTypeAnnotation', + }, + }, + ], + commands: [], + }, + }, + }, + }, +}; +const EVENT_PROPS = { + modules: { + Switch: { + type: 'Component', + components: { + EventsNativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [ + { + name: 'onChange', + optional: true, + bubblingType: 'bubble', + typeAnnotation: { + type: 'EventTypeAnnotation', + argument: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'value', + optional: false, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + { + name: 'source', + optional: true, + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + name: 'progress', + optional: true, + typeAnnotation: { + type: 'Int32TypeAnnotation', + }, + }, + { + name: 'scale', + optional: true, + typeAnnotation: { + type: 'FloatTypeAnnotation', + }, + }, + ], + }, + }, + }, + { + name: 'onArrayEventType', + optional: true, + bubblingType: 'bubble', + typeAnnotation: { + type: 'EventTypeAnnotation', + argument: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'bool_array_event_prop', + optional: false, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'BooleanTypeAnnotation', + }, + }, + }, + { + name: 'string_enum_event_prop', + optional: false, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'StringEnumTypeAnnotation', + options: ['YES', 'NO'], + }, + }, + }, + { + name: 'array_array_event_prop', + optional: false, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'StringTypeAnnotation', + }, + }, + }, + }, + { + name: 'array_object_event_prop', + optional: false, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'lat', + optional: false, + typeAnnotation: { + type: 'DoubleTypeAnnotation', + }, + }, + { + name: 'lon', + optional: false, + typeAnnotation: { + type: 'DoubleTypeAnnotation', + }, + }, + { + name: 'names', + optional: false, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'StringTypeAnnotation', + }, + }, + }, + ], + }, + }, + }, + { + name: 'array_mixed_event_prop', + optional: false, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'MixedTypeAnnotation', + }, + }, + }, + ], + }, + }, + }, + { + name: 'onEventDirect', + optional: true, + bubblingType: 'direct', + typeAnnotation: { + type: 'EventTypeAnnotation', + argument: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'value', + optional: false, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + ], + }, + }, + }, + { + name: 'onOrientationChange', + optional: true, + bubblingType: 'direct', + typeAnnotation: { + type: 'EventTypeAnnotation', + argument: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'orientation', + optional: false, + typeAnnotation: { + type: 'StringEnumTypeAnnotation', + options: ['landscape', 'portrait'], + }, + }, + ], + }, + }, + }, + { + name: 'onEnd', + optional: true, + bubblingType: 'bubble', + typeAnnotation: { + type: 'EventTypeAnnotation', + }, + }, + { + name: 'onEventWithMixedPropAttribute', + optional: true, + bubblingType: 'direct', + typeAnnotation: { + type: 'EventTypeAnnotation', + argument: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'value', + optional: false, + typeAnnotation: { + type: 'MixedTypeAnnotation', + }, + }, + ], + }, + }, + }, + ], + props: [ + { + name: 'disabled', + optional: true, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + default: false, + }, + }, + ], + commands: [], + }, + }, + }, + }, +}; +const EVENT_NESTED_OBJECT_PROPS = { + modules: { + Switch: { + type: 'Component', + components: { + EventsNestedObjectNativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [ + { + name: 'onChange', + optional: true, + bubblingType: 'bubble', + typeAnnotation: { + type: 'EventTypeAnnotation', + argument: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'location', + optional: false, + typeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'source', + optional: false, + typeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'url', + optional: false, + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'x', + optional: false, + typeAnnotation: { + type: 'Int32TypeAnnotation', + }, + }, + { + name: 'y', + optional: false, + typeAnnotation: { + type: 'Int32TypeAnnotation', + }, + }, + ], + }, + }, + ], + }, + }, + }, + ], + props: [ + { + name: 'disabled', + optional: true, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + default: false, + }, + }, + ], + commands: [], + }, + }, + }, + }, +}; +const TWO_COMPONENTS_SAME_FILE = { + modules: { + MyComponents: { + type: 'Component', + components: { + MultiComponent1NativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'disabled', + optional: true, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + default: false, + }, + }, + ], + commands: [], + }, + MultiComponent2NativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'disabled', + optional: true, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + default: true, + }, + }, + ], + commands: [], + }, + }, + }, + }, +}; +const TWO_COMPONENTS_DIFFERENT_FILES = { + modules: { + ComponentFile1: { + type: 'Component', + components: { + MultiFile1NativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'disabled', + optional: true, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + default: false, + }, + }, + ], + commands: [], + }, + }, + }, + ComponentFile2: { + type: 'Component', + components: { + MultiFile2NativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'disabled', + optional: true, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + default: true, + }, + }, + ], + commands: [], + }, + }, + }, + }, +}; +const COMMANDS = { + modules: { + Switch: { + type: 'Component', + components: { + CommandNativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [], + commands: [ + { + name: 'flashScrollIndicators', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + params: [], + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + }, + }, + { + name: 'allTypes', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + params: [ + { + name: 'x', + optional: false, + typeAnnotation: { + type: 'Int32TypeAnnotation', + }, + }, + { + name: 'y', + optional: false, + typeAnnotation: { + type: 'FloatTypeAnnotation', + }, + }, + { + name: 'z', + optional: false, + typeAnnotation: { + type: 'DoubleTypeAnnotation', + }, + }, + { + name: 'message', + optional: false, + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + name: 'animated', + optional: false, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + ], + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + }, + }, + ], + }, + }, + }, + }, +}; +const COMMANDS_AND_PROPS = { + modules: { + Switch: { + type: 'Component', + components: { + CommandNativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'accessibilityHint', + optional: true, + typeAnnotation: { + type: 'StringTypeAnnotation', + default: '', + }, + }, + ], + commands: [ + { + name: 'handleRootTag', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + params: [ + { + name: 'rootTag', + optional: false, + typeAnnotation: { + type: 'ReservedTypeAnnotation', + name: 'RootTag', + }, + }, + ], + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + }, + }, + { + name: 'hotspotUpdate', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + params: [ + { + name: 'x', + optional: false, + typeAnnotation: { + type: 'Int32TypeAnnotation', + }, + }, + { + name: 'y', + optional: false, + typeAnnotation: { + type: 'Int32TypeAnnotation', + }, + }, + ], + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + }, + }, + { + name: 'addItems', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + params: [ + { + name: 'items', + optional: false, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'StringTypeAnnotation', + }, + }, + }, + ], + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + }, + }, + ], + }, + }, + }, + }, +}; +const EXCLUDE_ANDROID = { + modules: { + ExcludedAndroid: { + type: 'Component', + components: { + ExcludedAndroidComponent: { + excludedPlatforms: ['android'], + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [], + commands: [], + }, + }, + }, + }, +}; +const EXCLUDE_ANDROID_IOS = { + modules: { + ExcludedAndroidIos: { + type: 'Component', + components: { + ExcludedAndroidIosComponent: { + excludedPlatforms: ['android', 'iOS'], + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [], + commands: [], + }, + }, + }, + }, +}; +const EXCLUDE_IOS_TWO_COMPONENTS_DIFFERENT_FILES = { + modules: { + ComponentFile1: { + type: 'Component', + components: { + ExcludedIosComponent: { + excludedPlatforms: ['iOS'], + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [], + commands: [], + }, + }, + }, + ComponentFile2: { + type: 'Component', + components: { + MultiFileIncludedNativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'disabled', + optional: true, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + default: true, + }, + }, + ], + commands: [], + }, + }, + }, + }, +}; +module.exports = { + NO_PROPS_NO_EVENTS, + INTERFACE_ONLY, + BOOLEAN_PROP, + STRING_PROP, + INTEGER_PROPS, + DOUBLE_PROPS, + FLOAT_PROPS, + COLOR_PROP, + IMAGE_PROP, + POINT_PROP, + INSETS_PROP, + DIMENSION_PROP, + ARRAY_PROPS, + ARRAY_PROPS_WITH_NESTED_OBJECT, + OBJECT_PROPS, + MULTI_NATIVE_PROP, + STRING_ENUM_PROP, + INT32_ENUM_PROP, + MIXED_PROP, + EVENT_PROPS, + EVENTS_WITH_PAPER_NAME, + EVENT_NESTED_OBJECT_PROPS, + TWO_COMPONENTS_SAME_FILE, + TWO_COMPONENTS_DIFFERENT_FILES, + COMMANDS, + COMMANDS_AND_PROPS, + EXCLUDE_ANDROID, + EXCLUDE_ANDROID_IOS, + EXCLUDE_IOS_TWO_COMPONENTS_DIFFERENT_FILES, +}; diff --git a/packages/react-native-codegen/lib/generators/components/__test_fixtures__/fixtures.js.flow b/packages/react-native-codegen/lib/generators/components/__test_fixtures__/fixtures.js.flow new file mode 100644 index 00000000000000..9121c8ad6878ee --- /dev/null +++ b/packages/react-native-codegen/lib/generators/components/__test_fixtures__/fixtures.js.flow @@ -0,0 +1,1911 @@ +/** + * 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-local + * @format + */ + +'use strict'; + +import type {SchemaType} from '../../../CodegenSchema.js'; + +const NO_PROPS_NO_EVENTS: SchemaType = { + modules: { + NoPropsNoEvents: { + type: 'Component', + components: { + NoPropsNoEventsComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [], + commands: [], + }, + }, + }, + }, +}; + +const INTERFACE_ONLY: SchemaType = { + modules: { + Switch: { + type: 'Component', + components: { + InterfaceOnlyComponent: { + interfaceOnly: true, + paperComponentName: 'RCTInterfaceOnlyComponent', + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [ + { + name: 'onChange', + optional: true, + bubblingType: 'bubble', + typeAnnotation: { + type: 'EventTypeAnnotation', + argument: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'value', + optional: false, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + ], + }, + }, + }, + ], + props: [ + { + name: 'accessibilityHint', + optional: true, + typeAnnotation: { + type: 'StringTypeAnnotation', + default: '', + }, + }, + ], + commands: [], + }, + }, + }, + }, +}; + +const EVENTS_WITH_PAPER_NAME: SchemaType = { + modules: { + Switch: { + type: 'Component', + components: { + InterfaceOnlyComponent: { + interfaceOnly: true, + paperComponentName: 'RCTInterfaceOnlyComponent', + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [ + { + name: 'onChange', + paperTopLevelNameDeprecated: 'paperChange', + optional: true, + bubblingType: 'bubble', + typeAnnotation: { + type: 'EventTypeAnnotation', + argument: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'value', + optional: false, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + ], + }, + }, + }, + { + name: 'onDirectChange', + paperTopLevelNameDeprecated: 'paperDirectChange', + optional: true, + bubblingType: 'direct', + typeAnnotation: { + type: 'EventTypeAnnotation', + argument: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'value', + optional: false, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + ], + }, + }, + }, + ], + props: [], + commands: [], + }, + }, + }, + }, +}; + +const BOOLEAN_PROP: SchemaType = { + modules: { + Switch: { + type: 'Component', + components: { + BooleanPropNativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'disabled', + optional: true, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + default: false, + }, + }, + ], + commands: [], + }, + }, + }, + }, +}; + +const STRING_PROP: SchemaType = { + modules: { + Switch: { + type: 'Component', + components: { + StringPropComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'accessibilityHint', + optional: true, + typeAnnotation: { + type: 'StringTypeAnnotation', + default: '', + }, + }, + { + name: 'accessibilityRole', + optional: true, + typeAnnotation: { + type: 'StringTypeAnnotation', + default: null, + }, + }, + ], + commands: [], + }, + }, + }, + }, +}; + +const INTEGER_PROPS: SchemaType = { + modules: { + Switch: { + type: 'Component', + components: { + IntegerPropNativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'progress1', + optional: true, + typeAnnotation: { + type: 'Int32TypeAnnotation', + default: 0, + }, + }, + { + name: 'progress2', + optional: true, + typeAnnotation: { + type: 'Int32TypeAnnotation', + default: -1, + }, + }, + { + name: 'progress3', + optional: true, + typeAnnotation: { + type: 'Int32TypeAnnotation', + default: 10, + }, + }, + ], + commands: [], + }, + }, + }, + }, +}; + +const FLOAT_PROPS: SchemaType = { + modules: { + Switch: { + type: 'Component', + components: { + FloatPropNativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'blurRadius', + optional: false, + typeAnnotation: { + type: 'FloatTypeAnnotation', + default: 0.0, + }, + }, + { + name: 'blurRadius2', + optional: true, + typeAnnotation: { + type: 'FloatTypeAnnotation', + default: 0.001, + }, + }, + { + name: 'blurRadius3', + optional: true, + typeAnnotation: { + type: 'FloatTypeAnnotation', + default: 2.1, + }, + }, + { + name: 'blurRadius4', + optional: true, + typeAnnotation: { + type: 'FloatTypeAnnotation', + default: 0, + }, + }, + { + name: 'blurRadius5', + optional: true, + typeAnnotation: { + type: 'FloatTypeAnnotation', + default: 1, + }, + }, + { + name: 'blurRadius6', + optional: true, + typeAnnotation: { + type: 'FloatTypeAnnotation', + default: -0.0, + }, + }, + ], + commands: [], + }, + }, + }, + }, +}; + +const DOUBLE_PROPS: SchemaType = { + modules: { + Switch: { + type: 'Component', + components: { + DoublePropNativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'blurRadius', + optional: false, + typeAnnotation: { + type: 'DoubleTypeAnnotation', + default: 0.0, + }, + }, + { + name: 'blurRadius2', + optional: true, + typeAnnotation: { + type: 'DoubleTypeAnnotation', + default: 0.001, + }, + }, + { + name: 'blurRadius3', + optional: true, + typeAnnotation: { + type: 'DoubleTypeAnnotation', + default: 2.1, + }, + }, + { + name: 'blurRadius4', + optional: true, + typeAnnotation: { + type: 'DoubleTypeAnnotation', + default: 0, + }, + }, + { + name: 'blurRadius5', + optional: true, + typeAnnotation: { + type: 'DoubleTypeAnnotation', + default: 1, + }, + }, + { + name: 'blurRadius6', + optional: true, + typeAnnotation: { + type: 'DoubleTypeAnnotation', + default: -0.0, + }, + }, + ], + commands: [], + }, + }, + }, + }, +}; + +const COLOR_PROP: SchemaType = { + modules: { + Switch: { + type: 'Component', + components: { + ColorPropNativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'tintColor', + optional: true, + typeAnnotation: { + type: 'ReservedPropTypeAnnotation', + name: 'ColorPrimitive', + }, + }, + ], + commands: [], + }, + }, + }, + }, +}; + +const IMAGE_PROP: SchemaType = { + modules: { + Slider: { + type: 'Component', + components: { + ImagePropNativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'thumbImage', + optional: true, + typeAnnotation: { + type: 'ReservedPropTypeAnnotation', + name: 'ImageSourcePrimitive', + }, + }, + ], + commands: [], + }, + }, + }, + }, +}; + +const POINT_PROP: SchemaType = { + modules: { + Switch: { + type: 'Component', + components: { + PointPropNativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'startPoint', + optional: true, + typeAnnotation: { + type: 'ReservedPropTypeAnnotation', + name: 'PointPrimitive', + }, + }, + ], + commands: [], + }, + }, + }, + }, +}; + +const INSETS_PROP: SchemaType = { + modules: { + ScrollView: { + type: 'Component', + components: { + InsetsPropNativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'contentInset', + optional: true, + typeAnnotation: { + type: 'ReservedPropTypeAnnotation', + name: 'EdgeInsetsPrimitive', + }, + }, + ], + commands: [], + }, + }, + }, + }, +}; + +const DIMENSION_PROP: SchemaType = { + modules: { + CustomView: { + type: 'Component', + components: { + DimensionPropNativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'marginBack', + optional: true, + typeAnnotation: { + type: 'ReservedPropTypeAnnotation', + name: 'DimensionPrimitive', + }, + }, + ], + commands: [], + }, + }, + }, + }, +}; + +const ARRAY_PROPS: SchemaType = { + modules: { + Slider: { + type: 'Component', + components: { + ArrayPropsNativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'names', + optional: true, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'StringTypeAnnotation', + }, + }, + }, + { + name: 'disableds', + optional: true, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'BooleanTypeAnnotation', + }, + }, + }, + { + name: 'progress', + optional: true, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'Int32TypeAnnotation', + }, + }, + }, + { + name: 'radii', + optional: true, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'FloatTypeAnnotation', + }, + }, + }, + { + name: 'colors', + optional: true, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'ReservedPropTypeAnnotation', + name: 'ColorPrimitive', + }, + }, + }, + { + name: 'srcs', + optional: true, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'ReservedPropTypeAnnotation', + name: 'ImageSourcePrimitive', + }, + }, + }, + { + name: 'points', + optional: true, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'ReservedPropTypeAnnotation', + name: 'PointPrimitive', + }, + }, + }, + { + name: 'dimensions', + optional: true, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'ReservedPropTypeAnnotation', + name: 'DimensionPrimitive', + }, + }, + }, + { + name: 'sizes', + optional: true, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'StringEnumTypeAnnotation', + default: 'small', + options: ['small', 'large'], + }, + }, + }, + { + name: 'object', + optional: true, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'stringProp', + optional: true, + typeAnnotation: { + type: 'StringTypeAnnotation', + default: '', + }, + }, + ], + }, + }, + }, + { + name: 'array', + optional: true, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'ObjectTypeAnnotation', + properties: [ + { + // This needs to stay the same as the object above + // to confirm that the structs are generated + // with unique non-colliding names + name: 'object', + optional: true, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'stringProp', + optional: true, + typeAnnotation: { + type: 'StringTypeAnnotation', + default: '', + }, + }, + ], + }, + }, + }, + ], + }, + }, + }, + { + name: 'arrayOfArrayOfObject', + optional: true, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'stringProp', + optional: true, + typeAnnotation: { + type: 'StringTypeAnnotation', + default: '', + }, + }, + ], + }, + }, + }, + }, + ], + commands: [], + }, + }, + }, + }, +}; + +const ARRAY_PROPS_WITH_NESTED_OBJECT: SchemaType = { + modules: { + Slider: { + type: 'Component', + components: { + ArrayPropsNativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'nativePrimitives', + optional: true, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'colors', + optional: true, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'ReservedPropTypeAnnotation', + name: 'ColorPrimitive', + }, + }, + }, + { + name: 'srcs', + optional: true, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'ReservedPropTypeAnnotation', + name: 'ImageSourcePrimitive', + }, + }, + }, + { + name: 'points', + optional: true, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'ReservedPropTypeAnnotation', + name: 'PointPrimitive', + }, + }, + }, + ], + }, + }, + }, + ], + commands: [], + }, + }, + }, + }, +}; + +const OBJECT_PROPS: SchemaType = { + modules: { + ObjectPropsNativeComponent: { + type: 'Component', + components: { + ObjectProps: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'objectProp', + optional: true, + typeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'stringProp', + optional: true, + typeAnnotation: { + type: 'StringTypeAnnotation', + default: '', + }, + }, + { + name: 'booleanProp', + optional: true, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + default: false, + }, + }, + { + name: 'floatProp', + optional: true, + typeAnnotation: { + type: 'FloatTypeAnnotation', + default: 0.0, + }, + }, + { + name: 'intProp', + optional: true, + typeAnnotation: { + type: 'Int32TypeAnnotation', + default: 0, + }, + }, + { + name: 'stringUserDefaultProp', + optional: true, + typeAnnotation: { + type: 'StringTypeAnnotation', + default: 'user_default', + }, + }, + { + name: 'booleanUserDefaultProp', + optional: true, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + default: true, + }, + }, + { + name: 'floatUserDefaultProp', + optional: true, + typeAnnotation: { + type: 'FloatTypeAnnotation', + default: 3.14, + }, + }, + { + name: 'intUserDefaultProp', + optional: true, + typeAnnotation: { + type: 'Int32TypeAnnotation', + default: 9999, + }, + }, + { + name: 'stringEnumProp', + optional: true, + typeAnnotation: { + type: 'StringEnumTypeAnnotation', + default: 'option1', + options: ['option1'], + }, + }, + { + name: 'intEnumProp', + optional: true, + typeAnnotation: { + type: 'Int32EnumTypeAnnotation', + default: 0, + options: [0], + }, + }, + { + name: 'objectArrayProp', + optional: false, + typeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'array', + optional: true, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'StringTypeAnnotation', + }, + }, + }, + ], + }, + }, + { + name: 'objectPrimitiveRequiredProp', + optional: false, + typeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'image', + optional: true, + typeAnnotation: { + type: 'ReservedPropTypeAnnotation', + name: 'ImageSourcePrimitive', + }, + }, + { + name: 'color', + optional: true, + typeAnnotation: { + type: 'ReservedPropTypeAnnotation', + name: 'ColorPrimitive', + }, + }, + { + name: 'point', + optional: true, + typeAnnotation: { + type: 'ReservedPropTypeAnnotation', + name: 'PointPrimitive', + }, + }, + ], + }, + }, + { + name: 'nestedPropA', + optional: false, + typeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'nestedPropB', + optional: false, + typeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'nestedPropC', + optional: true, + typeAnnotation: { + type: 'StringTypeAnnotation', + default: '', + }, + }, + ], + }, + }, + ], + }, + }, + { + name: 'nestedArrayAsProperty', + optional: false, + typeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'arrayProp', + optional: false, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'stringProp', + optional: false, + typeAnnotation: { + type: 'StringTypeAnnotation', + default: '', + }, + }, + ], + }, + }, + }, + ], + }, + }, + ], + }, + }, + ], + commands: [], + }, + }, + }, + }, +}; + +const MULTI_NATIVE_PROP: SchemaType = { + modules: { + Slider: { + type: 'Component', + components: { + ImageColorPropNativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'thumbImage', + optional: true, + typeAnnotation: { + type: 'ReservedPropTypeAnnotation', + name: 'ImageSourcePrimitive', + }, + }, + { + name: 'color', + optional: true, + typeAnnotation: { + type: 'ReservedPropTypeAnnotation', + name: 'ColorPrimitive', + }, + }, + { + name: 'thumbTintColor', + optional: true, + typeAnnotation: { + type: 'ReservedPropTypeAnnotation', + name: 'ColorPrimitive', + }, + }, + { + name: 'point', + optional: true, + typeAnnotation: { + type: 'ReservedPropTypeAnnotation', + name: 'PointPrimitive', + }, + }, + ], + commands: [], + }, + }, + }, + }, +}; + +const STRING_ENUM_PROP: SchemaType = { + modules: { + Switch: { + type: 'Component', + components: { + StringEnumPropsNativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'alignment', + optional: true, + typeAnnotation: { + type: 'StringEnumTypeAnnotation', + default: 'center', + options: ['top', 'center', 'bottom-right'], + }, + }, + ], + commands: [], + }, + }, + }, + }, +}; + +const INT32_ENUM_PROP: SchemaType = { + modules: { + Switch: { + type: 'Component', + components: { + Int32EnumPropsNativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'maxInterval', + optional: true, + typeAnnotation: { + type: 'Int32EnumTypeAnnotation', + default: 0, + options: [0, 1, 2], + }, + }, + ], + commands: [], + }, + }, + }, + }, +}; + +const MIXED_PROP: SchemaType = { + modules: { + CustomView: { + type: 'Component', + components: { + MixedPropNativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'mixedProp', + optional: false, + typeAnnotation: { + type: 'MixedTypeAnnotation', + }, + }, + ], + commands: [], + }, + }, + }, + }, +}; + +const EVENT_PROPS: SchemaType = { + modules: { + Switch: { + type: 'Component', + components: { + EventsNativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [ + { + name: 'onChange', + optional: true, + bubblingType: 'bubble', + typeAnnotation: { + type: 'EventTypeAnnotation', + argument: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'value', + optional: false, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + { + name: 'source', + optional: true, + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + name: 'progress', + optional: true, + typeAnnotation: { + type: 'Int32TypeAnnotation', + }, + }, + { + name: 'scale', + optional: true, + typeAnnotation: { + type: 'FloatTypeAnnotation', + }, + }, + ], + }, + }, + }, + { + name: 'onArrayEventType', + optional: true, + bubblingType: 'bubble', + typeAnnotation: { + type: 'EventTypeAnnotation', + argument: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'bool_array_event_prop', + optional: false, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'BooleanTypeAnnotation', + }, + }, + }, + { + name: 'string_enum_event_prop', + optional: false, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'StringEnumTypeAnnotation', + options: ['YES', 'NO'], + }, + }, + }, + { + name: 'array_array_event_prop', + optional: false, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'StringTypeAnnotation', + }, + }, + }, + }, + { + name: 'array_object_event_prop', + optional: false, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'lat', + optional: false, + typeAnnotation: { + type: 'DoubleTypeAnnotation', + }, + }, + { + name: 'lon', + optional: false, + typeAnnotation: { + type: 'DoubleTypeAnnotation', + }, + }, + { + name: 'names', + optional: false, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'StringTypeAnnotation', + }, + }, + }, + ], + }, + }, + }, + { + name: 'array_mixed_event_prop', + optional: false, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'MixedTypeAnnotation', + }, + }, + }, + ], + }, + }, + }, + { + name: 'onEventDirect', + optional: true, + bubblingType: 'direct', + typeAnnotation: { + type: 'EventTypeAnnotation', + argument: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'value', + optional: false, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + ], + }, + }, + }, + { + name: 'onOrientationChange', + optional: true, + bubblingType: 'direct', + typeAnnotation: { + type: 'EventTypeAnnotation', + argument: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'orientation', + optional: false, + typeAnnotation: { + type: 'StringEnumTypeAnnotation', + options: ['landscape', 'portrait'], + }, + }, + ], + }, + }, + }, + { + name: 'onEnd', + optional: true, + bubblingType: 'bubble', + typeAnnotation: { + type: 'EventTypeAnnotation', + }, + }, + { + name: 'onEventWithMixedPropAttribute', + optional: true, + bubblingType: 'direct', + typeAnnotation: { + type: 'EventTypeAnnotation', + argument: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'value', + optional: false, + typeAnnotation: { + type: 'MixedTypeAnnotation', + }, + }, + ], + }, + }, + }, + ], + props: [ + { + name: 'disabled', + optional: true, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + default: false, + }, + }, + ], + commands: [], + }, + }, + }, + }, +}; + +const EVENT_NESTED_OBJECT_PROPS: SchemaType = { + modules: { + Switch: { + type: 'Component', + components: { + EventsNestedObjectNativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [ + { + name: 'onChange', + optional: true, + bubblingType: 'bubble', + typeAnnotation: { + type: 'EventTypeAnnotation', + argument: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'location', + optional: false, + typeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'source', + optional: false, + typeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'url', + optional: false, + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'x', + optional: false, + typeAnnotation: { + type: 'Int32TypeAnnotation', + }, + }, + { + name: 'y', + optional: false, + typeAnnotation: { + type: 'Int32TypeAnnotation', + }, + }, + ], + }, + }, + ], + }, + }, + }, + ], + props: [ + { + name: 'disabled', + optional: true, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + default: false, + }, + }, + ], + commands: [], + }, + }, + }, + }, +}; + +const TWO_COMPONENTS_SAME_FILE: SchemaType = { + modules: { + MyComponents: { + type: 'Component', + components: { + MultiComponent1NativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'disabled', + optional: true, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + default: false, + }, + }, + ], + commands: [], + }, + + MultiComponent2NativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'disabled', + optional: true, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + default: true, + }, + }, + ], + commands: [], + }, + }, + }, + }, +}; + +const TWO_COMPONENTS_DIFFERENT_FILES: SchemaType = { + modules: { + ComponentFile1: { + type: 'Component', + components: { + MultiFile1NativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'disabled', + optional: true, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + default: false, + }, + }, + ], + commands: [], + }, + }, + }, + + ComponentFile2: { + type: 'Component', + components: { + MultiFile2NativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'disabled', + optional: true, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + default: true, + }, + }, + ], + commands: [], + }, + }, + }, + }, +}; + +const COMMANDS: SchemaType = { + modules: { + Switch: { + type: 'Component', + components: { + CommandNativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [], + commands: [ + { + name: 'flashScrollIndicators', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + params: [], + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + }, + }, + { + name: 'allTypes', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + params: [ + { + name: 'x', + optional: false, + typeAnnotation: { + type: 'Int32TypeAnnotation', + }, + }, + { + name: 'y', + optional: false, + typeAnnotation: { + type: 'FloatTypeAnnotation', + }, + }, + { + name: 'z', + optional: false, + typeAnnotation: { + type: 'DoubleTypeAnnotation', + }, + }, + { + name: 'message', + optional: false, + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + name: 'animated', + optional: false, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + ], + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + }, + }, + ], + }, + }, + }, + }, +}; + +const COMMANDS_AND_PROPS: SchemaType = { + modules: { + Switch: { + type: 'Component', + components: { + CommandNativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'accessibilityHint', + optional: true, + typeAnnotation: { + type: 'StringTypeAnnotation', + default: '', + }, + }, + ], + commands: [ + { + name: 'handleRootTag', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + params: [ + { + name: 'rootTag', + optional: false, + typeAnnotation: { + type: 'ReservedTypeAnnotation', + name: 'RootTag', + }, + }, + ], + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + }, + }, + { + name: 'hotspotUpdate', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + params: [ + { + name: 'x', + optional: false, + typeAnnotation: { + type: 'Int32TypeAnnotation', + }, + }, + { + name: 'y', + optional: false, + typeAnnotation: { + type: 'Int32TypeAnnotation', + }, + }, + ], + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + }, + }, + { + name: 'addItems', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + params: [ + { + name: 'items', + optional: false, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'StringTypeAnnotation', + }, + }, + }, + ], + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + }, + }, + ], + }, + }, + }, + }, +}; + +const EXCLUDE_ANDROID: SchemaType = { + modules: { + ExcludedAndroid: { + type: 'Component', + components: { + ExcludedAndroidComponent: { + excludedPlatforms: ['android'], + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [], + commands: [], + }, + }, + }, + }, +}; + +const EXCLUDE_ANDROID_IOS: SchemaType = { + modules: { + ExcludedAndroidIos: { + type: 'Component', + components: { + ExcludedAndroidIosComponent: { + excludedPlatforms: ['android', 'iOS'], + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [], + commands: [], + }, + }, + }, + }, +}; + +const EXCLUDE_IOS_TWO_COMPONENTS_DIFFERENT_FILES: SchemaType = { + modules: { + ComponentFile1: { + type: 'Component', + components: { + ExcludedIosComponent: { + excludedPlatforms: ['iOS'], + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [], + commands: [], + }, + }, + }, + ComponentFile2: { + type: 'Component', + components: { + MultiFileIncludedNativeComponent: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'disabled', + optional: true, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + default: true, + }, + }, + ], + commands: [], + }, + }, + }, + }, +}; + +module.exports = { + NO_PROPS_NO_EVENTS, + INTERFACE_ONLY, + BOOLEAN_PROP, + STRING_PROP, + INTEGER_PROPS, + DOUBLE_PROPS, + FLOAT_PROPS, + COLOR_PROP, + IMAGE_PROP, + POINT_PROP, + INSETS_PROP, + DIMENSION_PROP, + ARRAY_PROPS, + ARRAY_PROPS_WITH_NESTED_OBJECT, + OBJECT_PROPS, + MULTI_NATIVE_PROP, + STRING_ENUM_PROP, + INT32_ENUM_PROP, + MIXED_PROP, + EVENT_PROPS, + EVENTS_WITH_PAPER_NAME, + EVENT_NESTED_OBJECT_PROPS, + TWO_COMPONENTS_SAME_FILE, + TWO_COMPONENTS_DIFFERENT_FILES, + COMMANDS, + COMMANDS_AND_PROPS, + EXCLUDE_ANDROID, + EXCLUDE_ANDROID_IOS, + EXCLUDE_IOS_TWO_COMPONENTS_DIFFERENT_FILES, +}; diff --git a/packages/react-native-codegen/lib/generators/modules/GenerateModuleCpp.js b/packages/react-native-codegen/lib/generators/modules/GenerateModuleCpp.js new file mode 100644 index 00000000000000..fdc9f15fab298a --- /dev/null +++ b/packages/react-native-codegen/lib/generators/modules/GenerateModuleCpp.js @@ -0,0 +1,317 @@ +/** + * 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'; + +function _slicedToArray(r, e) { + return ( + _arrayWithHoles(r) || + _iterableToArrayLimit(r, e) || + _unsupportedIterableToArray(r, e) || + _nonIterableRest() + ); +} +function _nonIterableRest() { + throw new TypeError( + 'Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.', + ); +} +function _unsupportedIterableToArray(r, a) { + if (r) { + if ('string' == typeof r) return _arrayLikeToArray(r, a); + var t = {}.toString.call(r).slice(8, -1); + return ( + 'Object' === t && r.constructor && (t = r.constructor.name), + 'Map' === t || 'Set' === t + ? Array.from(r) + : 'Arguments' === t || + /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) + ? _arrayLikeToArray(r, a) + : void 0 + ); + } +} +function _arrayLikeToArray(r, a) { + (null == a || a > r.length) && (a = r.length); + for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; + return n; +} +function _iterableToArrayLimit(r, l) { + var t = + null == r + ? null + : ('undefined' != typeof Symbol && r[Symbol.iterator]) || r['@@iterator']; + if (null != t) { + var e, + n, + i, + u, + a = [], + f = !0, + o = !1; + try { + if (((i = (t = t.call(r)).next), 0 === l)) { + if (Object(t) !== t) return; + f = !1; + } else + for ( + ; + !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); + f = !0 + ); + } catch (r) { + (o = !0), (n = r); + } finally { + try { + if (!f && null != t.return && ((u = t.return()), Object(u) !== u)) + return; + } finally { + if (o) throw n; + } + } + return a; + } +} +function _arrayWithHoles(r) { + if (Array.isArray(r)) return r; +} +const _require = require('../../parsers/parsers-commons'), + unwrapNullable = _require.unwrapNullable; +const _require2 = require('./Utils'), + createAliasResolver = _require2.createAliasResolver, + getModules = _require2.getModules; +const HostFunctionTemplate = ({ + hasteModuleName, + methodName, + returnTypeAnnotation, + args, +}) => { + const isNullable = returnTypeAnnotation.type === 'NullableTypeAnnotation'; + const isVoid = returnTypeAnnotation.type === 'VoidTypeAnnotation'; + const methodCallArgs = [' rt', ...args].join(',\n '); + const methodCall = `static_cast<${hasteModuleName}CxxSpecJSI *>(&turboModule)->${methodName}(\n${methodCallArgs}\n )`; + return `static jsi::Value __hostFunction_${hasteModuleName}CxxSpecJSI_${methodName}(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {${ + isVoid + ? `\n ${methodCall};` + : isNullable + ? `\n auto result = ${methodCall};` + : '' + } + return ${ + isVoid + ? 'jsi::Value::undefined()' + : isNullable + ? 'result ? jsi::Value(std::move(*result)) : jsi::Value::null()' + : methodCall + }; +}`; +}; +const ModuleTemplate = ({ + hasteModuleName, + hostFunctions, + moduleName, + methods, +}) => { + return `${hostFunctions.join('\n')} + +${hasteModuleName}CxxSpecJSI::${hasteModuleName}CxxSpecJSI(std::shared_ptr jsInvoker) + : TurboModule("${moduleName}", jsInvoker) { +${methods + .map(({methodName, paramCount}) => { + return ` methodMap_["${methodName}"] = MethodMetadata {${paramCount}, __hostFunction_${hasteModuleName}CxxSpecJSI_${methodName}};`; + }) + .join('\n')} +}`; +}; +const FileTemplate = ({libraryName, modules}) => { + return `/** + * 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: GenerateModuleCpp.js + */ + +#include "${libraryName}JSI.h" + +namespace facebook::react { + +${modules} + + +} // namespace facebook::react +`; +}; +function serializeArg(moduleName, arg, index, resolveAlias, enumMap) { + const nullableTypeAnnotation = arg.typeAnnotation, + optional = arg.optional; + const _unwrapNullable = unwrapNullable(nullableTypeAnnotation), + _unwrapNullable2 = _slicedToArray(_unwrapNullable, 2), + typeAnnotation = _unwrapNullable2[0], + nullable = _unwrapNullable2[1]; + let realTypeAnnotation = typeAnnotation; + if (realTypeAnnotation.type === 'TypeAliasTypeAnnotation') { + realTypeAnnotation = resolveAlias(realTypeAnnotation.name); + } + function wrap(callback) { + const val = `args[${index}]`; + const expression = callback(val); + + // param?: T + if (optional && !nullable) { + // throw new Error('are we hitting this case? ' + moduleName); + return `count <= ${index} || ${val}.isUndefined() ? std::nullopt : std::make_optional(${expression})`; + } + + // param: ?T + // param?: ?T + if (nullable || optional) { + return `count <= ${index} || ${val}.isNull() || ${val}.isUndefined() ? std::nullopt : std::make_optional(${expression})`; + } + + // param: T + return `count <= ${index} ? throw jsi::JSError(rt, "Expected argument in position ${index} to be passed") : ${expression}`; + } + switch (realTypeAnnotation.type) { + case 'ReservedTypeAnnotation': + switch (realTypeAnnotation.name) { + case 'RootTag': + return wrap(val => `${val}.asNumber()`); + default: + realTypeAnnotation.name; + throw new Error( + `Unknown prop type for "${arg.name}, found: ${realTypeAnnotation.name}"`, + ); + } + case 'StringTypeAnnotation': + return wrap(val => `${val}.asString(rt)`); + case 'BooleanTypeAnnotation': + return wrap(val => `${val}.asBool()`); + case 'EnumDeclaration': + switch (realTypeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return wrap(val => `${val}.asNumber()`); + case 'StringTypeAnnotation': + return wrap(val => `${val}.asString(rt)`); + default: + throw new Error( + `Unknown enum type for "${arg.name}, found: ${realTypeAnnotation.type}"`, + ); + } + case 'NumberTypeAnnotation': + return wrap(val => `${val}.asNumber()`); + case 'FloatTypeAnnotation': + return wrap(val => `${val}.asNumber()`); + case 'DoubleTypeAnnotation': + return wrap(val => `${val}.asNumber()`); + case 'Int32TypeAnnotation': + return wrap(val => `${val}.asNumber()`); + case 'ArrayTypeAnnotation': + return wrap(val => `${val}.asObject(rt).asArray(rt)`); + case 'FunctionTypeAnnotation': + return wrap(val => `${val}.asObject(rt).asFunction(rt)`); + case 'GenericObjectTypeAnnotation': + return wrap(val => `${val}.asObject(rt)`); + case 'UnionTypeAnnotation': + switch (typeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return wrap(val => `${val}.asNumber()`); + case 'ObjectTypeAnnotation': + return wrap(val => `${val}.asObject(rt)`); + case 'StringTypeAnnotation': + return wrap(val => `${val}.asString(rt)`); + default: + throw new Error( + `Unsupported union member type for param "${arg.name}, found: ${realTypeAnnotation.memberType}"`, + ); + } + case 'ObjectTypeAnnotation': + return wrap(val => `${val}.asObject(rt)`); + case 'MixedTypeAnnotation': + return wrap(val => `jsi::Value(rt, ${val})`); + default: + realTypeAnnotation.type; + throw new Error( + `Unknown prop type for "${arg.name}, found: ${realTypeAnnotation.type}"`, + ); + } +} +function serializePropertyIntoHostFunction( + moduleName, + hasteModuleName, + property, + resolveAlias, + enumMap, +) { + const _unwrapNullable3 = unwrapNullable(property.typeAnnotation), + _unwrapNullable4 = _slicedToArray(_unwrapNullable3, 1), + propertyTypeAnnotation = _unwrapNullable4[0]; + return HostFunctionTemplate({ + hasteModuleName, + methodName: property.name, + returnTypeAnnotation: propertyTypeAnnotation.returnTypeAnnotation, + args: propertyTypeAnnotation.params.map((p, i) => + serializeArg(moduleName, p, i, resolveAlias, enumMap), + ), + }); +} +module.exports = { + generate( + libraryName, + schema, + packageName, + assumeNonnull = false, + headerPrefix, + ) { + const nativeModules = getModules(schema); + const modules = Object.keys(nativeModules) + .map(hasteModuleName => { + const nativeModule = nativeModules[hasteModuleName]; + const aliasMap = nativeModule.aliasMap, + enumMap = nativeModule.enumMap, + methods = nativeModule.spec.methods, + moduleName = nativeModule.moduleName; + const resolveAlias = createAliasResolver(aliasMap); + const hostFunctions = methods.map(property => + serializePropertyIntoHostFunction( + moduleName, + hasteModuleName, + property, + resolveAlias, + enumMap, + ), + ); + return ModuleTemplate({ + hasteModuleName, + hostFunctions, + moduleName, + methods: methods.map( + ({name: propertyName, typeAnnotation: nullableTypeAnnotation}) => { + const _unwrapNullable5 = unwrapNullable(nullableTypeAnnotation), + _unwrapNullable6 = _slicedToArray(_unwrapNullable5, 1), + params = _unwrapNullable6[0].params; + return { + methodName: propertyName, + paramCount: params.length, + }; + }, + ), + }); + }) + .join('\n'); + const fileName = `${libraryName}JSI-generated.cpp`; + const replacedTemplate = FileTemplate({ + modules, + libraryName, + }); + return new Map([[fileName, replacedTemplate]]); + }, +}; diff --git a/packages/react-native-codegen/lib/generators/modules/GenerateModuleCpp.js.flow b/packages/react-native-codegen/lib/generators/modules/GenerateModuleCpp.js.flow new file mode 100644 index 00000000000000..f0e5ac171b95bd --- /dev/null +++ b/packages/react-native-codegen/lib/generators/modules/GenerateModuleCpp.js.flow @@ -0,0 +1,290 @@ +/** + * 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 { + NamedShape, + NativeModuleEnumMap, + NativeModuleFunctionTypeAnnotation, + NativeModuleParamTypeAnnotation, + NativeModulePropertyShape, + NativeModuleTypeAnnotation, + Nullable, + SchemaType, +} from '../../CodegenSchema'; +import type {AliasResolver} from './Utils'; + +const {unwrapNullable} = require('../../parsers/parsers-commons'); +const {createAliasResolver, getModules} = require('./Utils'); + +type FilesOutput = Map; + +const HostFunctionTemplate = ({ + hasteModuleName, + methodName, + returnTypeAnnotation, + args, +}: $ReadOnly<{ + hasteModuleName: string, + methodName: string, + returnTypeAnnotation: Nullable, + args: Array, +}>) => { + const isNullable = returnTypeAnnotation.type === 'NullableTypeAnnotation'; + const isVoid = returnTypeAnnotation.type === 'VoidTypeAnnotation'; + const methodCallArgs = [' rt', ...args].join(',\n '); + const methodCall = `static_cast<${hasteModuleName}CxxSpecJSI *>(&turboModule)->${methodName}(\n${methodCallArgs}\n )`; + + return `static jsi::Value __hostFunction_${hasteModuleName}CxxSpecJSI_${methodName}(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {${ + isVoid + ? `\n ${methodCall};` + : isNullable + ? `\n auto result = ${methodCall};` + : '' + } + return ${ + isVoid + ? 'jsi::Value::undefined()' + : isNullable + ? 'result ? jsi::Value(std::move(*result)) : jsi::Value::null()' + : methodCall + }; +}`; +}; + +const ModuleTemplate = ({ + hasteModuleName, + hostFunctions, + moduleName, + methods, +}: $ReadOnly<{ + hasteModuleName: string, + hostFunctions: $ReadOnlyArray, + moduleName: string, + methods: $ReadOnlyArray<$ReadOnly<{methodName: string, paramCount: number}>>, +}>) => { + return `${hostFunctions.join('\n')} + +${hasteModuleName}CxxSpecJSI::${hasteModuleName}CxxSpecJSI(std::shared_ptr jsInvoker) + : TurboModule("${moduleName}", jsInvoker) { +${methods + .map(({methodName, paramCount}) => { + return ` methodMap_["${methodName}"] = MethodMetadata {${paramCount}, __hostFunction_${hasteModuleName}CxxSpecJSI_${methodName}};`; + }) + .join('\n')} +}`; +}; + +const FileTemplate = ({ + libraryName, + modules, +}: $ReadOnly<{ + libraryName: string, + modules: string, +}>) => { + return `/** + * 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: GenerateModuleCpp.js + */ + +#include "${libraryName}JSI.h" + +namespace facebook::react { + +${modules} + + +} // namespace facebook::react +`; +}; + +type Param = NamedShape>; + +function serializeArg( + moduleName: string, + arg: Param, + index: number, + resolveAlias: AliasResolver, + enumMap: NativeModuleEnumMap, +): string { + const {typeAnnotation: nullableTypeAnnotation, optional} = arg; + const [typeAnnotation, nullable] = + unwrapNullable(nullableTypeAnnotation); + + let realTypeAnnotation = typeAnnotation; + if (realTypeAnnotation.type === 'TypeAliasTypeAnnotation') { + realTypeAnnotation = resolveAlias(realTypeAnnotation.name); + } + + function wrap(callback: (val: string) => string) { + const val = `args[${index}]`; + const expression = callback(val); + + // param?: T + if (optional && !nullable) { + // throw new Error('are we hitting this case? ' + moduleName); + return `count <= ${index} || ${val}.isUndefined() ? std::nullopt : std::make_optional(${expression})`; + } + + // param: ?T + // param?: ?T + if (nullable || optional) { + return `count <= ${index} || ${val}.isNull() || ${val}.isUndefined() ? std::nullopt : std::make_optional(${expression})`; + } + + // param: T + return `count <= ${index} ? throw jsi::JSError(rt, "Expected argument in position ${index} to be passed") : ${expression}`; + } + + switch (realTypeAnnotation.type) { + case 'ReservedTypeAnnotation': + switch (realTypeAnnotation.name) { + case 'RootTag': + return wrap(val => `${val}.asNumber()`); + default: + (realTypeAnnotation.name: empty); + throw new Error( + `Unknown prop type for "${arg.name}, found: ${realTypeAnnotation.name}"`, + ); + } + case 'StringTypeAnnotation': + return wrap(val => `${val}.asString(rt)`); + case 'BooleanTypeAnnotation': + return wrap(val => `${val}.asBool()`); + case 'EnumDeclaration': + switch (realTypeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return wrap(val => `${val}.asNumber()`); + case 'StringTypeAnnotation': + return wrap(val => `${val}.asString(rt)`); + default: + throw new Error( + `Unknown enum type for "${arg.name}, found: ${realTypeAnnotation.type}"`, + ); + } + case 'NumberTypeAnnotation': + return wrap(val => `${val}.asNumber()`); + case 'FloatTypeAnnotation': + return wrap(val => `${val}.asNumber()`); + case 'DoubleTypeAnnotation': + return wrap(val => `${val}.asNumber()`); + case 'Int32TypeAnnotation': + return wrap(val => `${val}.asNumber()`); + case 'ArrayTypeAnnotation': + return wrap(val => `${val}.asObject(rt).asArray(rt)`); + case 'FunctionTypeAnnotation': + return wrap(val => `${val}.asObject(rt).asFunction(rt)`); + case 'GenericObjectTypeAnnotation': + return wrap(val => `${val}.asObject(rt)`); + case 'UnionTypeAnnotation': + switch (typeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return wrap(val => `${val}.asNumber()`); + case 'ObjectTypeAnnotation': + return wrap(val => `${val}.asObject(rt)`); + case 'StringTypeAnnotation': + return wrap(val => `${val}.asString(rt)`); + default: + throw new Error( + `Unsupported union member type for param "${arg.name}, found: ${realTypeAnnotation.memberType}"`, + ); + } + case 'ObjectTypeAnnotation': + return wrap(val => `${val}.asObject(rt)`); + case 'MixedTypeAnnotation': + return wrap(val => `jsi::Value(rt, ${val})`); + default: + (realTypeAnnotation.type: empty); + throw new Error( + `Unknown prop type for "${arg.name}, found: ${realTypeAnnotation.type}"`, + ); + } +} + +function serializePropertyIntoHostFunction( + moduleName: string, + hasteModuleName: string, + property: NativeModulePropertyShape, + resolveAlias: AliasResolver, + enumMap: NativeModuleEnumMap, +): string { + const [propertyTypeAnnotation] = + unwrapNullable(property.typeAnnotation); + + return HostFunctionTemplate({ + hasteModuleName, + methodName: property.name, + returnTypeAnnotation: propertyTypeAnnotation.returnTypeAnnotation, + args: propertyTypeAnnotation.params.map((p, i) => + serializeArg(moduleName, p, i, resolveAlias, enumMap), + ), + }); +} + +module.exports = { + generate( + libraryName: string, + schema: SchemaType, + packageName?: string, + assumeNonnull: boolean = false, + headerPrefix?: string, + ): FilesOutput { + const nativeModules = getModules(schema); + + const modules = Object.keys(nativeModules) + .map((hasteModuleName: string) => { + const nativeModule = nativeModules[hasteModuleName]; + const { + aliasMap, + enumMap, + spec: {methods}, + moduleName, + } = nativeModule; + const resolveAlias = createAliasResolver(aliasMap); + const hostFunctions = methods.map(property => + serializePropertyIntoHostFunction( + moduleName, + hasteModuleName, + property, + resolveAlias, + enumMap, + ), + ); + + return ModuleTemplate({ + hasteModuleName, + hostFunctions, + moduleName, + methods: methods.map( + ({name: propertyName, typeAnnotation: nullableTypeAnnotation}) => { + const [{params}] = unwrapNullable(nullableTypeAnnotation); + return { + methodName: propertyName, + paramCount: params.length, + }; + }, + ), + }); + }) + .join('\n'); + + const fileName = `${libraryName}JSI-generated.cpp`; + const replacedTemplate = FileTemplate({ + modules, + libraryName, + }); + return new Map([[fileName, replacedTemplate]]); + }, +}; diff --git a/packages/react-native-codegen/lib/generators/modules/GenerateModuleH.js b/packages/react-native-codegen/lib/generators/modules/GenerateModuleH.js new file mode 100644 index 00000000000000..13232a10502cba --- /dev/null +++ b/packages/react-native-codegen/lib/generators/modules/GenerateModuleH.js @@ -0,0 +1,643 @@ +/** + * 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'; + +function _slicedToArray(r, e) { + return ( + _arrayWithHoles(r) || + _iterableToArrayLimit(r, e) || + _unsupportedIterableToArray(r, e) || + _nonIterableRest() + ); +} +function _nonIterableRest() { + throw new TypeError( + 'Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.', + ); +} +function _unsupportedIterableToArray(r, a) { + if (r) { + if ('string' == typeof r) return _arrayLikeToArray(r, a); + var t = {}.toString.call(r).slice(8, -1); + return ( + 'Object' === t && r.constructor && (t = r.constructor.name), + 'Map' === t || 'Set' === t + ? Array.from(r) + : 'Arguments' === t || + /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) + ? _arrayLikeToArray(r, a) + : void 0 + ); + } +} +function _arrayLikeToArray(r, a) { + (null == a || a > r.length) && (a = r.length); + for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; + return n; +} +function _iterableToArrayLimit(r, l) { + var t = + null == r + ? null + : ('undefined' != typeof Symbol && r[Symbol.iterator]) || r['@@iterator']; + if (null != t) { + var e, + n, + i, + u, + a = [], + f = !0, + o = !1; + try { + if (((i = (t = t.call(r)).next), 0 === l)) { + if (Object(t) !== t) return; + f = !1; + } else + for ( + ; + !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); + f = !0 + ); + } catch (r) { + (o = !0), (n = r); + } finally { + try { + if (!f && null != t.return && ((u = t.return()), Object(u) !== u)) + return; + } finally { + if (o) throw n; + } + } + return a; + } +} +function _arrayWithHoles(r) { + if (Array.isArray(r)) return r; +} +const _require = require('../../parsers/parsers-commons'), + unwrapNullable = _require.unwrapNullable; +const _require2 = require('../TypeUtils/Cxx'), + wrapOptional = _require2.wrapOptional; +const _require3 = require('../Utils'), + getEnumName = _require3.getEnumName, + toPascalCase = _require3.toPascalCase, + toSafeCppString = _require3.toSafeCppString; +const _require4 = require('../Utils'), + indent = _require4.indent; +const _require5 = require('./Utils'), + createAliasResolver = _require5.createAliasResolver, + getModules = _require5.getModules, + isArrayRecursiveMember = _require5.isArrayRecursiveMember, + isDirectRecursiveMember = _require5.isDirectRecursiveMember; +const ModuleClassDeclarationTemplate = ({ + hasteModuleName, + moduleProperties, + structs, + enums, +}) => { + return `${enums} + ${structs}class JSI_EXPORT ${hasteModuleName}CxxSpecJSI : public TurboModule { +protected: + ${hasteModuleName}CxxSpecJSI(std::shared_ptr jsInvoker); + +public: + ${indent(moduleProperties.join('\n'), 2)} + +};`; +}; +const ModuleSpecClassDeclarationTemplate = ({ + hasteModuleName, + moduleName, + moduleEventEmitters, + moduleProperties, +}) => { + return `template +class JSI_EXPORT ${hasteModuleName}CxxSpec : public TurboModule { +public: + jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &propName) override { + return delegate_.get(rt, propName); + } + + static constexpr std::string_view kModuleName = "${moduleName}"; + +protected: + ${hasteModuleName}CxxSpec(std::shared_ptr jsInvoker) + : TurboModule(std::string{${hasteModuleName}CxxSpec::kModuleName}, jsInvoker), + delegate_(reinterpret_cast(this), jsInvoker) {} +${moduleEventEmitters.map(e => e.emitFunction).join('\n')} + +private: + class Delegate : public ${hasteModuleName}CxxSpecJSI { + public: + Delegate(T *instance, std::shared_ptr jsInvoker) : + ${hasteModuleName}CxxSpecJSI(std::move(jsInvoker)), instance_(instance) { +${moduleEventEmitters.map(e => e.registerEventEmitter).join('\n')} + } + + ${indent(moduleProperties.join('\n'), 4)} + + private: + friend class ${hasteModuleName}CxxSpec; + T *instance_; + }; + + Delegate delegate_; +};`; +}; +const FileTemplate = ({modules}) => { + return `/** + * 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: GenerateModuleH.js + */ + +#pragma once + +#include +#include + +namespace facebook::react { + +${modules.join('\n\n')} + +} // namespace facebook::react +`; +}; +function translatePrimitiveJSTypeToCpp( + moduleName, + parentObjectAliasName, + nullableTypeAnnotation, + optional, + createErrorMessage, + resolveAlias, + enumMap, +) { + const _unwrapNullable = unwrapNullable(nullableTypeAnnotation), + _unwrapNullable2 = _slicedToArray(_unwrapNullable, 2), + typeAnnotation = _unwrapNullable2[0], + nullable = _unwrapNullable2[1]; + const isRecursiveType = isDirectRecursiveMember( + parentObjectAliasName, + nullableTypeAnnotation, + ); + const isRequired = (!optional && !nullable) || isRecursiveType; + let realTypeAnnotation = typeAnnotation; + if (realTypeAnnotation.type === 'TypeAliasTypeAnnotation') { + realTypeAnnotation = resolveAlias(realTypeAnnotation.name); + } + switch (realTypeAnnotation.type) { + case 'ReservedTypeAnnotation': + switch (realTypeAnnotation.name) { + case 'RootTag': + return wrapOptional('double', isRequired); + default: + realTypeAnnotation.name; + throw new Error(createErrorMessage(realTypeAnnotation.name)); + } + case 'VoidTypeAnnotation': + return 'void'; + case 'StringTypeAnnotation': + return wrapOptional('jsi::String', isRequired); + case 'NumberTypeAnnotation': + return wrapOptional('double', isRequired); + case 'DoubleTypeAnnotation': + return wrapOptional('double', isRequired); + case 'FloatTypeAnnotation': + return wrapOptional('double', isRequired); + case 'Int32TypeAnnotation': + return wrapOptional('int', isRequired); + case 'BooleanTypeAnnotation': + return wrapOptional('bool', isRequired); + case 'EnumDeclaration': + switch (realTypeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return wrapOptional('jsi::Value', isRequired); + case 'StringTypeAnnotation': + return wrapOptional('jsi::String', isRequired); + default: + throw new Error(createErrorMessage(realTypeAnnotation.type)); + } + case 'GenericObjectTypeAnnotation': + return wrapOptional('jsi::Object', isRequired); + case 'UnionTypeAnnotation': + switch (typeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return wrapOptional('double', isRequired); + case 'ObjectTypeAnnotation': + return wrapOptional('jsi::Object', isRequired); + case 'StringTypeAnnotation': + return wrapOptional('jsi::String', isRequired); + default: + throw new Error(createErrorMessage(realTypeAnnotation.type)); + } + case 'ObjectTypeAnnotation': + return wrapOptional('jsi::Object', isRequired); + case 'ArrayTypeAnnotation': + return wrapOptional('jsi::Array', isRequired); + case 'FunctionTypeAnnotation': + return wrapOptional('jsi::Function', isRequired); + case 'PromiseTypeAnnotation': + return wrapOptional('jsi::Value', isRequired); + case 'MixedTypeAnnotation': + return wrapOptional('jsi::Value', isRequired); + default: + realTypeAnnotation.type; + throw new Error(createErrorMessage(realTypeAnnotation.type)); + } +} +function createStructsString(hasteModuleName, aliasMap, resolveAlias, enumMap) { + const getCppType = (parentObjectAlias, v) => + translatePrimitiveJSTypeToCpp( + hasteModuleName, + parentObjectAlias, + v.typeAnnotation, + false, + typeName => `Unsupported type for param "${v.name}". Found: ${typeName}`, + resolveAlias, + enumMap, + ); + return Object.keys(aliasMap) + .map(alias => { + const value = aliasMap[alias]; + if (value.properties.length === 0) { + return ''; + } + const structName = `${hasteModuleName}${alias}`; + const templateParameter = value.properties.filter( + v => + !isDirectRecursiveMember(alias, v.typeAnnotation) && + !isArrayRecursiveMember(alias, v.typeAnnotation), + ); + const templateParameterWithTypename = templateParameter + .map((v, i) => `typename P${i}`) + .join(', '); + const templateParameterWithoutTypename = templateParameter + .map((v, i) => `P${i}`) + .join(', '); + let i = -1; + const templateMemberTypes = value.properties.map(v => { + if (isDirectRecursiveMember(alias, v.typeAnnotation)) { + return `std::unique_ptr<${structName}<${templateParameterWithoutTypename}>> ${v.name}`; + } else if (isArrayRecursiveMember(alias, v.typeAnnotation)) { + const _unwrapNullable3 = unwrapNullable(v.typeAnnotation), + _unwrapNullable4 = _slicedToArray(_unwrapNullable3, 1), + nullable = _unwrapNullable4[0]; + return ( + (nullable + ? `std::optional>>` + : `std::vector<${structName}<${templateParameterWithoutTypename}>>`) + + ` ${v.name}` + ); + } else { + i++; + return `P${i} ${v.name}`; + } + }); + const debugParameterConversion = value.properties + .map( + v => ` static ${getCppType(alias, v)} ${ + v.name + }ToJs(jsi::Runtime &rt, decltype(types.${v.name}) value) { + return bridging::toJs(rt, value); + }`, + ) + .join('\n\n'); + return ` +#pragma mark - ${structName} + +template <${templateParameterWithTypename}> +struct ${structName} { +${templateMemberTypes.map(v => ' ' + v).join(';\n')}; + bool operator==(const ${structName} &other) const { + return ${value.properties + .map(v => `${v.name} == other.${v.name}`) + .join(' && ')}; + } +}; + +template +struct ${structName}Bridging { + static T types; + + static T fromJs( + jsi::Runtime &rt, + const jsi::Object &value, + const std::shared_ptr &jsInvoker) { + T result{ +${value.properties + .map(v => { + if (isDirectRecursiveMember(alias, v.typeAnnotation)) { + return ` value.hasProperty(rt, "${v.name}") ? std::make_unique(bridging::fromJs(rt, value.getProperty(rt, "${v.name}"), jsInvoker)) : nullptr`; + } else { + return ` bridging::fromJs(rt, value.getProperty(rt, "${v.name}"), jsInvoker)`; + } + }) + .join(',\n')}}; + return result; + } + +#ifdef DEBUG +${debugParameterConversion} +#endif + + static jsi::Object toJs( + jsi::Runtime &rt, + const T &value, + const std::shared_ptr &jsInvoker) { + auto result = facebook::jsi::Object(rt); +${value.properties + .map(v => { + if (isDirectRecursiveMember(alias, v.typeAnnotation)) { + return ` if (value.${v.name}) { + result.setProperty(rt, "${v.name}", bridging::toJs(rt, *value.${v.name}, jsInvoker)); + }`; + } else if (v.optional) { + return ` if (value.${v.name}) { + result.setProperty(rt, "${v.name}", bridging::toJs(rt, value.${v.name}.value(), jsInvoker)); + }`; + } else { + return ` result.setProperty(rt, "${v.name}", bridging::toJs(rt, value.${v.name}, jsInvoker));`; + } + }) + .join('\n')} + return result; + } +}; + +`; + }) + .join('\n'); +} +const EnumTemplate = ({ + enumName, + values, + fromCases, + toCases, + nativeEnumMemberType, +}) => { + const _ref = + nativeEnumMemberType === 'std::string' + ? [ + 'const jsi::String &rawValue', + 'std::string value = rawValue.utf8(rt);', + 'jsi::String', + ] + : [ + 'const jsi::Value &rawValue', + 'double value = (double)rawValue.asNumber();', + 'jsi::Value', + ], + _ref2 = _slicedToArray(_ref, 3), + fromValue = _ref2[0], + fromValueConversion = _ref2[1], + toValue = _ref2[2]; + return ` +#pragma mark - ${enumName} + +enum class ${enumName} { ${values} }; + +template <> +struct Bridging<${enumName}> { + static ${enumName} fromJs(jsi::Runtime &rt, ${fromValue}) { + ${fromValueConversion} + ${fromCases} + } + + static ${toValue} toJs(jsi::Runtime &rt, ${enumName} value) { + ${toCases} + } +};`; +}; +function generateEnum(hasteModuleName, origEnumName, members, memberType) { + const enumName = getEnumName(hasteModuleName, origEnumName); + const nativeEnumMemberType = + memberType === 'StringTypeAnnotation' ? 'std::string' : 'int32_t'; + const getMemberValueAppearance = value => + memberType === 'StringTypeAnnotation' ? `"${value}"` : `${value}`; + const fromCases = + members + .map( + member => `if (value == ${getMemberValueAppearance(member.value)}) { + return ${enumName}::${toSafeCppString(member.name)}; + }`, + ) + .join(' else ') + + ` else { + throw jsi::JSError(rt, "No appropriate enum member found for value"); + }`; + const toCases = + members + .map( + member => `if (value == ${enumName}::${toSafeCppString(member.name)}) { + return bridging::toJs(rt, ${getMemberValueAppearance(member.value)}); + }`, + ) + .join(' else ') + + ` else { + throw jsi::JSError(rt, "No appropriate enum member found for enum value"); + }`; + return EnumTemplate({ + enumName, + values: members.map(member => toSafeCppString(member.name)).join(', '), + fromCases, + toCases, + nativeEnumMemberType, + }); +} +function createEnums(hasteModuleName, enumMap, resolveAlias) { + return Object.entries(enumMap) + .map(([enumName, enumNode]) => { + return generateEnum( + hasteModuleName, + enumName, + enumNode.members, + enumNode.memberType, + ); + }) + .filter(Boolean) + .join('\n'); +} +function translatePropertyToCpp( + hasteModuleName, + prop, + resolveAlias, + enumMap, + abstract = false, +) { + const _unwrapNullable5 = unwrapNullable(prop.typeAnnotation), + _unwrapNullable6 = _slicedToArray(_unwrapNullable5, 1), + propTypeAnnotation = _unwrapNullable6[0]; + const params = propTypeAnnotation.params.map( + param => `std::move(${param.name})`, + ); + const paramTypes = propTypeAnnotation.params.map(param => { + const translatedParam = translatePrimitiveJSTypeToCpp( + hasteModuleName, + null, + param.typeAnnotation, + param.optional, + typeName => + `Unsupported type for param "${param.name}" in ${prop.name}. Found: ${typeName}`, + resolveAlias, + enumMap, + ); + return `${translatedParam} ${param.name}`; + }); + const returnType = translatePrimitiveJSTypeToCpp( + hasteModuleName, + null, + propTypeAnnotation.returnTypeAnnotation, + false, + typeName => `Unsupported return type for ${prop.name}. Found: ${typeName}`, + resolveAlias, + enumMap, + ); + + // The first param will always be the runtime reference. + paramTypes.unshift('jsi::Runtime &rt'); + const method = `${returnType} ${prop.name}(${paramTypes.join(', ')})`; + if (abstract) { + return `virtual ${method} = 0;`; + } + return `${method} override { + static_assert( + bridging::getParameterCount(&T::${prop.name}) == ${paramTypes.length}, + "Expected ${prop.name}(...) to have ${paramTypes.length} parameters"); + + return bridging::callFromJs<${returnType}>( + rt, &T::${prop.name}, jsInvoker_, ${['instance_', ...params].join(', ')}); +}`; +} +function translateEventEmitterToCpp( + moduleName, + eventEmitter, + resolveAlias, + enumMap, +) { + const isVoidTypeAnnotation = + eventEmitter.typeAnnotation.typeAnnotation.type === 'VoidTypeAnnotation'; + const templateName = `${toPascalCase(eventEmitter.name)}Type`; + const jsiType = translatePrimitiveJSTypeToCpp( + moduleName, + null, + eventEmitter.typeAnnotation.typeAnnotation, + false, + typeName => + `Unsupported type for eventEmitter "${eventEmitter.name}" in ${moduleName}. Found: ${typeName}`, + resolveAlias, + enumMap, + ); + const isArray = jsiType === 'jsi::Array'; + return { + isVoidTypeAnnotation: isVoidTypeAnnotation, + templateName: isVoidTypeAnnotation ? `/*${templateName}*/` : templateName, + registerEventEmitter: ` eventEmitterMap_["${ + eventEmitter.name + }"] = std::make_shared>();`, + emitFunction: ` + ${ + isVoidTypeAnnotation ? '' : `template ` + }void emit${toPascalCase(eventEmitter.name)}(${ + isVoidTypeAnnotation + ? '' + : `${isArray ? `std::vector<${templateName}>` : templateName} value` + }) {${ + isVoidTypeAnnotation + ? '' + : ` + static_assert(bridging::supportsFromJs<${ + isArray ? `std::vector<${templateName}>` : templateName + }, ${jsiType}>, "value cannnot be converted to ${jsiType}");` + } + static_cast&>(*delegate_.eventEmitterMap_["${eventEmitter.name}"]).emit(${ + isVoidTypeAnnotation + ? '' + : `[jsInvoker = jsInvoker_, eventValue = value](jsi::Runtime& rt) -> jsi::Value { + return bridging::toJs(rt, eventValue, jsInvoker); + }` + }); + }`, + }; +} +module.exports = { + generate( + libraryName, + schema, + packageName, + assumeNonnull = false, + headerPrefix, + ) { + const nativeModules = getModules(schema); + const modules = Object.keys(nativeModules).flatMap(hasteModuleName => { + const _nativeModules$hasteM = nativeModules[hasteModuleName], + aliasMap = _nativeModules$hasteM.aliasMap, + enumMap = _nativeModules$hasteM.enumMap, + spec = _nativeModules$hasteM.spec, + moduleName = _nativeModules$hasteM.moduleName; + const resolveAlias = createAliasResolver(aliasMap); + const structs = createStructsString( + hasteModuleName, + aliasMap, + resolveAlias, + enumMap, + ); + const enums = createEnums(hasteModuleName, enumMap, resolveAlias); + return [ + ModuleClassDeclarationTemplate({ + hasteModuleName, + moduleProperties: spec.methods.map(prop => + translatePropertyToCpp( + hasteModuleName, + prop, + resolveAlias, + enumMap, + true, + ), + ), + structs, + enums, + }), + ModuleSpecClassDeclarationTemplate({ + hasteModuleName, + moduleName, + moduleEventEmitters: spec.eventEmitters.map(eventEmitter => + translateEventEmitterToCpp( + moduleName, + eventEmitter, + resolveAlias, + enumMap, + ), + ), + moduleProperties: spec.methods.map(prop => + translatePropertyToCpp( + hasteModuleName, + prop, + resolveAlias, + enumMap, + ), + ), + }), + ]; + }); + const fileName = `${libraryName}JSI.h`; + const replacedTemplate = FileTemplate({ + modules, + }); + return new Map([[fileName, replacedTemplate]]); + }, +}; diff --git a/packages/react-native-codegen/lib/generators/modules/GenerateModuleH.js.flow b/packages/react-native-codegen/lib/generators/modules/GenerateModuleH.js.flow new file mode 100644 index 00000000000000..783cf37f1c28d0 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/modules/GenerateModuleH.js.flow @@ -0,0 +1,649 @@ +/** + * 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 { + NamedShape, + NativeModuleBaseTypeAnnotation, +} from '../../CodegenSchema'; +import type { + NativeModuleAliasMap, + NativeModuleEnumMap, + NativeModuleEnumMembers, + NativeModuleEnumMemberType, + NativeModuleEventEmitterShape, + NativeModuleFunctionTypeAnnotation, + NativeModulePropertyShape, + NativeModuleTypeAnnotation, + Nullable, + SchemaType, +} from '../../CodegenSchema'; +import type {AliasResolver} from './Utils'; + +const {unwrapNullable} = require('../../parsers/parsers-commons'); +const {wrapOptional} = require('../TypeUtils/Cxx'); +const {getEnumName, toPascalCase, toSafeCppString} = require('../Utils'); +const {indent} = require('../Utils'); +const { + createAliasResolver, + getModules, + isArrayRecursiveMember, + isDirectRecursiveMember, +} = require('./Utils'); + +type FilesOutput = Map; + +const ModuleClassDeclarationTemplate = ({ + hasteModuleName, + moduleProperties, + structs, + enums, +}: $ReadOnly<{ + hasteModuleName: string, + moduleProperties: string[], + structs: string, + enums: string, +}>) => { + return `${enums} + ${structs}class JSI_EXPORT ${hasteModuleName}CxxSpecJSI : public TurboModule { +protected: + ${hasteModuleName}CxxSpecJSI(std::shared_ptr jsInvoker); + +public: + ${indent(moduleProperties.join('\n'), 2)} + +};`; +}; + +const ModuleSpecClassDeclarationTemplate = ({ + hasteModuleName, + moduleName, + moduleEventEmitters, + moduleProperties, +}: $ReadOnly<{ + hasteModuleName: string, + moduleName: string, + moduleEventEmitters: EventEmitterCpp[], + moduleProperties: string[], +}>) => { + return `template +class JSI_EXPORT ${hasteModuleName}CxxSpec : public TurboModule { +public: + jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &propName) override { + return delegate_.get(rt, propName); + } + + static constexpr std::string_view kModuleName = "${moduleName}"; + +protected: + ${hasteModuleName}CxxSpec(std::shared_ptr jsInvoker) + : TurboModule(std::string{${hasteModuleName}CxxSpec::kModuleName}, jsInvoker), + delegate_(reinterpret_cast(this), jsInvoker) {} +${moduleEventEmitters.map(e => e.emitFunction).join('\n')} + +private: + class Delegate : public ${hasteModuleName}CxxSpecJSI { + public: + Delegate(T *instance, std::shared_ptr jsInvoker) : + ${hasteModuleName}CxxSpecJSI(std::move(jsInvoker)), instance_(instance) { +${moduleEventEmitters.map(e => e.registerEventEmitter).join('\n')} + } + + ${indent(moduleProperties.join('\n'), 4)} + + private: + friend class ${hasteModuleName}CxxSpec; + T *instance_; + }; + + Delegate delegate_; +};`; +}; + +const FileTemplate = ({ + modules, +}: $ReadOnly<{ + modules: string[], +}>) => { + return `/** + * 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: GenerateModuleH.js + */ + +#pragma once + +#include +#include + +namespace facebook::react { + +${modules.join('\n\n')} + +} // namespace facebook::react +`; +}; + +function translatePrimitiveJSTypeToCpp( + moduleName: string, + parentObjectAliasName: ?string, + nullableTypeAnnotation: Nullable, + optional: boolean, + createErrorMessage: (typeName: string) => string, + resolveAlias: AliasResolver, + enumMap: NativeModuleEnumMap, +) { + const [typeAnnotation, nullable] = unwrapNullable( + nullableTypeAnnotation, + ); + const isRecursiveType = isDirectRecursiveMember( + parentObjectAliasName, + nullableTypeAnnotation, + ); + const isRequired = (!optional && !nullable) || isRecursiveType; + let realTypeAnnotation = typeAnnotation; + if (realTypeAnnotation.type === 'TypeAliasTypeAnnotation') { + realTypeAnnotation = resolveAlias(realTypeAnnotation.name); + } + + switch (realTypeAnnotation.type) { + case 'ReservedTypeAnnotation': + switch (realTypeAnnotation.name) { + case 'RootTag': + return wrapOptional('double', isRequired); + default: + (realTypeAnnotation.name: empty); + throw new Error(createErrorMessage(realTypeAnnotation.name)); + } + case 'VoidTypeAnnotation': + return 'void'; + case 'StringTypeAnnotation': + return wrapOptional('jsi::String', isRequired); + case 'NumberTypeAnnotation': + return wrapOptional('double', isRequired); + case 'DoubleTypeAnnotation': + return wrapOptional('double', isRequired); + case 'FloatTypeAnnotation': + return wrapOptional('double', isRequired); + case 'Int32TypeAnnotation': + return wrapOptional('int', isRequired); + case 'BooleanTypeAnnotation': + return wrapOptional('bool', isRequired); + case 'EnumDeclaration': + switch (realTypeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return wrapOptional('jsi::Value', isRequired); + case 'StringTypeAnnotation': + return wrapOptional('jsi::String', isRequired); + default: + throw new Error(createErrorMessage(realTypeAnnotation.type)); + } + case 'GenericObjectTypeAnnotation': + return wrapOptional('jsi::Object', isRequired); + case 'UnionTypeAnnotation': + switch (typeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return wrapOptional('double', isRequired); + case 'ObjectTypeAnnotation': + return wrapOptional('jsi::Object', isRequired); + case 'StringTypeAnnotation': + return wrapOptional('jsi::String', isRequired); + default: + throw new Error(createErrorMessage(realTypeAnnotation.type)); + } + case 'ObjectTypeAnnotation': + return wrapOptional('jsi::Object', isRequired); + case 'ArrayTypeAnnotation': + return wrapOptional('jsi::Array', isRequired); + case 'FunctionTypeAnnotation': + return wrapOptional('jsi::Function', isRequired); + case 'PromiseTypeAnnotation': + return wrapOptional('jsi::Value', isRequired); + case 'MixedTypeAnnotation': + return wrapOptional('jsi::Value', isRequired); + default: + (realTypeAnnotation.type: empty); + throw new Error(createErrorMessage(realTypeAnnotation.type)); + } +} + +function createStructsString( + hasteModuleName: string, + aliasMap: NativeModuleAliasMap, + resolveAlias: AliasResolver, + enumMap: NativeModuleEnumMap, +): string { + const getCppType = ( + parentObjectAlias: string, + v: NamedShape>, + ) => + translatePrimitiveJSTypeToCpp( + hasteModuleName, + parentObjectAlias, + v.typeAnnotation, + false, + typeName => `Unsupported type for param "${v.name}". Found: ${typeName}`, + resolveAlias, + enumMap, + ); + + return Object.keys(aliasMap) + .map(alias => { + const value = aliasMap[alias]; + if (value.properties.length === 0) { + return ''; + } + const structName = `${hasteModuleName}${alias}`; + const templateParameter = value.properties.filter( + v => + !isDirectRecursiveMember(alias, v.typeAnnotation) && + !isArrayRecursiveMember(alias, v.typeAnnotation), + ); + const templateParameterWithTypename = templateParameter + .map((v, i) => `typename P${i}`) + .join(', '); + const templateParameterWithoutTypename = templateParameter + .map((v, i) => `P${i}`) + .join(', '); + let i = -1; + const templateMemberTypes = value.properties.map(v => { + if (isDirectRecursiveMember(alias, v.typeAnnotation)) { + return `std::unique_ptr<${structName}<${templateParameterWithoutTypename}>> ${v.name}`; + } else if (isArrayRecursiveMember(alias, v.typeAnnotation)) { + const [nullable] = unwrapNullable( + v.typeAnnotation, + ); + return ( + (nullable + ? `std::optional>>` + : `std::vector<${structName}<${templateParameterWithoutTypename}>>`) + + ` ${v.name}` + ); + } else { + i++; + return `P${i} ${v.name}`; + } + }); + const debugParameterConversion = value.properties + .map( + v => ` static ${getCppType(alias, v)} ${ + v.name + }ToJs(jsi::Runtime &rt, decltype(types.${v.name}) value) { + return bridging::toJs(rt, value); + }`, + ) + .join('\n\n'); + return ` +#pragma mark - ${structName} + +template <${templateParameterWithTypename}> +struct ${structName} { +${templateMemberTypes.map(v => ' ' + v).join(';\n')}; + bool operator==(const ${structName} &other) const { + return ${value.properties + .map(v => `${v.name} == other.${v.name}`) + .join(' && ')}; + } +}; + +template +struct ${structName}Bridging { + static T types; + + static T fromJs( + jsi::Runtime &rt, + const jsi::Object &value, + const std::shared_ptr &jsInvoker) { + T result{ +${value.properties + .map(v => { + if (isDirectRecursiveMember(alias, v.typeAnnotation)) { + return ` value.hasProperty(rt, "${v.name}") ? std::make_unique(bridging::fromJs(rt, value.getProperty(rt, "${v.name}"), jsInvoker)) : nullptr`; + } else { + return ` bridging::fromJs(rt, value.getProperty(rt, "${v.name}"), jsInvoker)`; + } + }) + .join(',\n')}}; + return result; + } + +#ifdef DEBUG +${debugParameterConversion} +#endif + + static jsi::Object toJs( + jsi::Runtime &rt, + const T &value, + const std::shared_ptr &jsInvoker) { + auto result = facebook::jsi::Object(rt); +${value.properties + .map(v => { + if (isDirectRecursiveMember(alias, v.typeAnnotation)) { + return ` if (value.${v.name}) { + result.setProperty(rt, "${v.name}", bridging::toJs(rt, *value.${v.name}, jsInvoker)); + }`; + } else if (v.optional) { + return ` if (value.${v.name}) { + result.setProperty(rt, "${v.name}", bridging::toJs(rt, value.${v.name}.value(), jsInvoker)); + }`; + } else { + return ` result.setProperty(rt, "${v.name}", bridging::toJs(rt, value.${v.name}, jsInvoker));`; + } + }) + .join('\n')} + return result; + } +}; + +`; + }) + .join('\n'); +} + +type NativeEnumMemberValueType = 'std::string' | 'int32_t'; + +const EnumTemplate = ({ + enumName, + values, + fromCases, + toCases, + nativeEnumMemberType, +}: { + enumName: string, + values: string, + fromCases: string, + toCases: string, + nativeEnumMemberType: NativeEnumMemberValueType, +}) => { + const [fromValue, fromValueConversion, toValue] = + nativeEnumMemberType === 'std::string' + ? [ + 'const jsi::String &rawValue', + 'std::string value = rawValue.utf8(rt);', + 'jsi::String', + ] + : [ + 'const jsi::Value &rawValue', + 'double value = (double)rawValue.asNumber();', + 'jsi::Value', + ]; + + return ` +#pragma mark - ${enumName} + +enum class ${enumName} { ${values} }; + +template <> +struct Bridging<${enumName}> { + static ${enumName} fromJs(jsi::Runtime &rt, ${fromValue}) { + ${fromValueConversion} + ${fromCases} + } + + static ${toValue} toJs(jsi::Runtime &rt, ${enumName} value) { + ${toCases} + } +};`; +}; + +function generateEnum( + hasteModuleName: string, + origEnumName: string, + members: NativeModuleEnumMembers, + memberType: NativeModuleEnumMemberType, +): string { + const enumName = getEnumName(hasteModuleName, origEnumName); + + const nativeEnumMemberType: NativeEnumMemberValueType = + memberType === 'StringTypeAnnotation' ? 'std::string' : 'int32_t'; + + const getMemberValueAppearance = (value: string) => + memberType === 'StringTypeAnnotation' ? `"${value}"` : `${value}`; + + const fromCases = + members + .map( + member => `if (value == ${getMemberValueAppearance(member.value)}) { + return ${enumName}::${toSafeCppString(member.name)}; + }`, + ) + .join(' else ') + + ` else { + throw jsi::JSError(rt, "No appropriate enum member found for value"); + }`; + + const toCases = + members + .map( + member => `if (value == ${enumName}::${toSafeCppString(member.name)}) { + return bridging::toJs(rt, ${getMemberValueAppearance(member.value)}); + }`, + ) + .join(' else ') + + ` else { + throw jsi::JSError(rt, "No appropriate enum member found for enum value"); + }`; + + return EnumTemplate({ + enumName, + values: members.map(member => toSafeCppString(member.name)).join(', '), + fromCases, + toCases, + nativeEnumMemberType, + }); +} + +function createEnums( + hasteModuleName: string, + enumMap: NativeModuleEnumMap, + resolveAlias: AliasResolver, +): string { + return Object.entries(enumMap) + .map(([enumName, enumNode]) => { + return generateEnum( + hasteModuleName, + enumName, + enumNode.members, + enumNode.memberType, + ); + }) + .filter(Boolean) + .join('\n'); +} + +function translatePropertyToCpp( + hasteModuleName: string, + prop: NativeModulePropertyShape, + resolveAlias: AliasResolver, + enumMap: NativeModuleEnumMap, + abstract: boolean = false, +): string { + const [propTypeAnnotation] = + unwrapNullable(prop.typeAnnotation); + + const params = propTypeAnnotation.params.map( + param => `std::move(${param.name})`, + ); + + const paramTypes = propTypeAnnotation.params.map(param => { + const translatedParam = translatePrimitiveJSTypeToCpp( + hasteModuleName, + null, + param.typeAnnotation, + param.optional, + typeName => + `Unsupported type for param "${param.name}" in ${prop.name}. Found: ${typeName}`, + resolveAlias, + enumMap, + ); + return `${translatedParam} ${param.name}`; + }); + + const returnType = translatePrimitiveJSTypeToCpp( + hasteModuleName, + null, + propTypeAnnotation.returnTypeAnnotation, + false, + typeName => `Unsupported return type for ${prop.name}. Found: ${typeName}`, + resolveAlias, + enumMap, + ); + + // The first param will always be the runtime reference. + paramTypes.unshift('jsi::Runtime &rt'); + + const method = `${returnType} ${prop.name}(${paramTypes.join(', ')})`; + + if (abstract) { + return `virtual ${method} = 0;`; + } + + return `${method} override { + static_assert( + bridging::getParameterCount(&T::${prop.name}) == ${paramTypes.length}, + "Expected ${prop.name}(...) to have ${paramTypes.length} parameters"); + + return bridging::callFromJs<${returnType}>( + rt, &T::${prop.name}, jsInvoker_, ${['instance_', ...params].join(', ')}); +}`; +} + +type EventEmitterCpp = { + isVoidTypeAnnotation: boolean, + templateName: string, + registerEventEmitter: string, + emitFunction: string, +}; + +function translateEventEmitterToCpp( + moduleName: string, + eventEmitter: NativeModuleEventEmitterShape, + resolveAlias: AliasResolver, + enumMap: NativeModuleEnumMap, +): EventEmitterCpp { + const isVoidTypeAnnotation = + eventEmitter.typeAnnotation.typeAnnotation.type === 'VoidTypeAnnotation'; + const templateName = `${toPascalCase(eventEmitter.name)}Type`; + const jsiType = translatePrimitiveJSTypeToCpp( + moduleName, + null, + eventEmitter.typeAnnotation.typeAnnotation, + false, + typeName => + `Unsupported type for eventEmitter "${eventEmitter.name}" in ${moduleName}. Found: ${typeName}`, + resolveAlias, + enumMap, + ); + const isArray = jsiType === 'jsi::Array'; + return { + isVoidTypeAnnotation: isVoidTypeAnnotation, + templateName: isVoidTypeAnnotation ? `/*${templateName}*/` : templateName, + registerEventEmitter: ` eventEmitterMap_["${ + eventEmitter.name + }"] = std::make_shared>();`, + emitFunction: ` + ${ + isVoidTypeAnnotation ? '' : `template ` + }void emit${toPascalCase(eventEmitter.name)}(${ + isVoidTypeAnnotation + ? '' + : `${isArray ? `std::vector<${templateName}>` : templateName} value` + }) {${ + isVoidTypeAnnotation + ? '' + : ` + static_assert(bridging::supportsFromJs<${ + isArray ? `std::vector<${templateName}>` : templateName + }, ${jsiType}>, "value cannnot be converted to ${jsiType}");` + } + static_cast&>(*delegate_.eventEmitterMap_["${eventEmitter.name}"]).emit(${ + isVoidTypeAnnotation + ? '' + : `[jsInvoker = jsInvoker_, eventValue = value](jsi::Runtime& rt) -> jsi::Value { + return bridging::toJs(rt, eventValue, jsInvoker); + }` + }); + }`, + }; +} + +module.exports = { + generate( + libraryName: string, + schema: SchemaType, + packageName?: string, + assumeNonnull: boolean = false, + headerPrefix?: string, + ): FilesOutput { + const nativeModules = getModules(schema); + + const modules = Object.keys(nativeModules).flatMap(hasteModuleName => { + const {aliasMap, enumMap, spec, moduleName} = + nativeModules[hasteModuleName]; + const resolveAlias = createAliasResolver(aliasMap); + const structs = createStructsString( + hasteModuleName, + aliasMap, + resolveAlias, + enumMap, + ); + const enums = createEnums(hasteModuleName, enumMap, resolveAlias); + + return [ + ModuleClassDeclarationTemplate({ + hasteModuleName, + moduleProperties: spec.methods.map(prop => + translatePropertyToCpp( + hasteModuleName, + prop, + resolveAlias, + enumMap, + true, + ), + ), + structs, + enums, + }), + ModuleSpecClassDeclarationTemplate({ + hasteModuleName, + moduleName, + moduleEventEmitters: spec.eventEmitters.map(eventEmitter => + translateEventEmitterToCpp( + moduleName, + eventEmitter, + resolveAlias, + enumMap, + ), + ), + moduleProperties: spec.methods.map(prop => + translatePropertyToCpp( + hasteModuleName, + prop, + resolveAlias, + enumMap, + ), + ), + }), + ]; + }); + + const fileName = `${libraryName}JSI.h`; + const replacedTemplate = FileTemplate({modules}); + + return new Map([[fileName, replacedTemplate]]); + }, +}; diff --git a/packages/react-native-codegen/lib/generators/modules/GenerateModuleJavaSpec.js b/packages/react-native-codegen/lib/generators/modules/GenerateModuleJavaSpec.js new file mode 100644 index 00000000000000..9e06a387703f0f --- /dev/null +++ b/packages/react-native-codegen/lib/generators/modules/GenerateModuleJavaSpec.js @@ -0,0 +1,569 @@ +/** + * 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'; + +function _slicedToArray(r, e) { + return ( + _arrayWithHoles(r) || + _iterableToArrayLimit(r, e) || + _unsupportedIterableToArray(r, e) || + _nonIterableRest() + ); +} +function _nonIterableRest() { + throw new TypeError( + 'Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.', + ); +} +function _unsupportedIterableToArray(r, a) { + if (r) { + if ('string' == typeof r) return _arrayLikeToArray(r, a); + var t = {}.toString.call(r).slice(8, -1); + return ( + 'Object' === t && r.constructor && (t = r.constructor.name), + 'Map' === t || 'Set' === t + ? Array.from(r) + : 'Arguments' === t || + /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) + ? _arrayLikeToArray(r, a) + : void 0 + ); + } +} +function _arrayLikeToArray(r, a) { + (null == a || a > r.length) && (a = r.length); + for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; + return n; +} +function _iterableToArrayLimit(r, l) { + var t = + null == r + ? null + : ('undefined' != typeof Symbol && r[Symbol.iterator]) || r['@@iterator']; + if (null != t) { + var e, + n, + i, + u, + a = [], + f = !0, + o = !1; + try { + if (((i = (t = t.call(r)).next), 0 === l)) { + if (Object(t) !== t) return; + f = !1; + } else + for ( + ; + !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); + f = !0 + ); + } catch (r) { + (o = !0), (n = r); + } finally { + try { + if (!f && null != t.return && ((u = t.return()), Object(u) !== u)) + return; + } finally { + if (o) throw n; + } + } + return a; + } +} +function _arrayWithHoles(r) { + if (Array.isArray(r)) return r; +} +const _require = require('../../parsers/parsers-commons'), + unwrapNullable = _require.unwrapNullable; +const _require2 = require('../TypeUtils/Java'), + wrapOptional = _require2.wrapOptional; +const _require3 = require('./Utils'), + createAliasResolver = _require3.createAliasResolver, + getModules = _require3.getModules; +function FileTemplate(config) { + const packageName = config.packageName, + className = config.className, + jsName = config.jsName, + methods = config.methods, + imports = config.imports; + return ` +/** + * 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: GenerateModuleJavaSpec.js + * + * ${'@'}nolint + */ + +package ${packageName}; + +${imports} + +public abstract class ${className} extends ReactContextBaseJavaModule implements TurboModule { + public static final String NAME = "${jsName}"; + + public ${className}(ReactApplicationContext reactContext) { + super(reactContext); + } + + @Override + public @Nonnull String getName() { + return NAME; + } + +${methods} +} +`; +} +function MethodTemplate(config) { + const abstract = config.abstract, + methodBody = config.methodBody, + methodJavaAnnotation = config.methodJavaAnnotation, + methodName = config.methodName, + translatedReturnType = config.translatedReturnType, + traversedArgs = config.traversedArgs; + const methodQualifier = abstract ? 'abstract ' : ''; + const methodClosing = abstract + ? ';' + : methodBody != null && methodBody.length > 0 + ? ` { ${methodBody} }` + : ' {}'; + return ` ${methodJavaAnnotation} + public ${methodQualifier}${translatedReturnType} ${methodName}(${traversedArgs.join( + ', ', + )})${methodClosing}`; +} +function translateFunctionParamToJavaType( + param, + createErrorMessage, + resolveAlias, + imports, +) { + const optional = param.optional, + nullableTypeAnnotation = param.typeAnnotation; + const _unwrapNullable = unwrapNullable(nullableTypeAnnotation), + _unwrapNullable2 = _slicedToArray(_unwrapNullable, 2), + typeAnnotation = _unwrapNullable2[0], + nullable = _unwrapNullable2[1]; + const isRequired = !optional && !nullable; + if (!isRequired) { + imports.add('javax.annotation.Nullable'); + } + + // FIXME: support class alias for args + let realTypeAnnotation = typeAnnotation; + if (realTypeAnnotation.type === 'TypeAliasTypeAnnotation') { + realTypeAnnotation = resolveAlias(realTypeAnnotation.name); + } + switch (realTypeAnnotation.type) { + case 'ReservedTypeAnnotation': + switch (realTypeAnnotation.name) { + case 'RootTag': + return wrapOptional('double', isRequired); + default: + realTypeAnnotation.name; + throw new Error(createErrorMessage(realTypeAnnotation.name)); + } + case 'StringTypeAnnotation': + return wrapOptional('String', isRequired); + case 'NumberTypeAnnotation': + return wrapOptional('double', isRequired); + case 'FloatTypeAnnotation': + return wrapOptional('double', isRequired); + case 'DoubleTypeAnnotation': + return wrapOptional('double', isRequired); + case 'Int32TypeAnnotation': + return wrapOptional('double', isRequired); + case 'BooleanTypeAnnotation': + return wrapOptional('boolean', isRequired); + case 'EnumDeclaration': + switch (realTypeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return wrapOptional('double', isRequired); + case 'StringTypeAnnotation': + return wrapOptional('String', isRequired); + default: + throw new Error(createErrorMessage(realTypeAnnotation.type)); + } + case 'UnionTypeAnnotation': + switch (typeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return wrapOptional('double', isRequired); + case 'ObjectTypeAnnotation': + imports.add('com.facebook.react.bridge.ReadableMap'); + return wrapOptional('ReadableMap', isRequired); + case 'StringTypeAnnotation': + return wrapOptional('String', isRequired); + default: + throw new Error( + `Unsupported union member returning value, found: ${realTypeAnnotation.memberType}"`, + ); + } + case 'ObjectTypeAnnotation': + imports.add('com.facebook.react.bridge.ReadableMap'); + return wrapOptional('ReadableMap', isRequired); + case 'GenericObjectTypeAnnotation': + // Treat this the same as ObjectTypeAnnotation for now. + imports.add('com.facebook.react.bridge.ReadableMap'); + return wrapOptional('ReadableMap', isRequired); + case 'ArrayTypeAnnotation': + imports.add('com.facebook.react.bridge.ReadableArray'); + return wrapOptional('ReadableArray', isRequired); + case 'FunctionTypeAnnotation': + imports.add('com.facebook.react.bridge.Callback'); + return wrapOptional('Callback', isRequired); + default: + realTypeAnnotation.type; + throw new Error(createErrorMessage(realTypeAnnotation.type)); + } +} +function translateFunctionReturnTypeToJavaType( + nullableReturnTypeAnnotation, + createErrorMessage, + resolveAlias, + imports, +) { + const _unwrapNullable3 = unwrapNullable(nullableReturnTypeAnnotation), + _unwrapNullable4 = _slicedToArray(_unwrapNullable3, 2), + returnTypeAnnotation = _unwrapNullable4[0], + nullable = _unwrapNullable4[1]; + if (nullable) { + imports.add('javax.annotation.Nullable'); + } + const isRequired = !nullable; + + // FIXME: support class alias for args + let realTypeAnnotation = returnTypeAnnotation; + if (realTypeAnnotation.type === 'TypeAliasTypeAnnotation') { + realTypeAnnotation = resolveAlias(realTypeAnnotation.name); + } + switch (realTypeAnnotation.type) { + case 'ReservedTypeAnnotation': + switch (realTypeAnnotation.name) { + case 'RootTag': + return wrapOptional('double', isRequired); + default: + realTypeAnnotation.name; + throw new Error(createErrorMessage(realTypeAnnotation.name)); + } + case 'VoidTypeAnnotation': + return 'void'; + case 'PromiseTypeAnnotation': + return 'void'; + case 'StringTypeAnnotation': + return wrapOptional('String', isRequired); + case 'NumberTypeAnnotation': + return wrapOptional('double', isRequired); + case 'FloatTypeAnnotation': + return wrapOptional('double', isRequired); + case 'DoubleTypeAnnotation': + return wrapOptional('double', isRequired); + case 'Int32TypeAnnotation': + return wrapOptional('double', isRequired); + case 'BooleanTypeAnnotation': + return wrapOptional('boolean', isRequired); + case 'EnumDeclaration': + switch (realTypeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return wrapOptional('double', isRequired); + case 'StringTypeAnnotation': + return wrapOptional('String', isRequired); + default: + throw new Error(createErrorMessage(realTypeAnnotation.type)); + } + case 'UnionTypeAnnotation': + switch (realTypeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return wrapOptional('double', isRequired); + case 'ObjectTypeAnnotation': + imports.add('com.facebook.react.bridge.WritableMap'); + return wrapOptional('WritableMap', isRequired); + case 'StringTypeAnnotation': + return wrapOptional('String', isRequired); + default: + throw new Error( + `Unsupported union member returning value, found: ${realTypeAnnotation.memberType}"`, + ); + } + case 'ObjectTypeAnnotation': + imports.add('com.facebook.react.bridge.WritableMap'); + return wrapOptional('WritableMap', isRequired); + case 'GenericObjectTypeAnnotation': + imports.add('com.facebook.react.bridge.WritableMap'); + return wrapOptional('WritableMap', isRequired); + case 'ArrayTypeAnnotation': + imports.add('com.facebook.react.bridge.WritableArray'); + return wrapOptional('WritableArray', isRequired); + default: + realTypeAnnotation.type; + throw new Error(createErrorMessage(realTypeAnnotation.type)); + } +} +function getFalsyReturnStatementFromReturnType( + nullableReturnTypeAnnotation, + createErrorMessage, + resolveAlias, +) { + const _unwrapNullable5 = unwrapNullable(nullableReturnTypeAnnotation), + _unwrapNullable6 = _slicedToArray(_unwrapNullable5, 2), + returnTypeAnnotation = _unwrapNullable6[0], + nullable = _unwrapNullable6[1]; + let realTypeAnnotation = returnTypeAnnotation; + if (realTypeAnnotation.type === 'TypeAliasTypeAnnotation') { + realTypeAnnotation = resolveAlias(realTypeAnnotation.name); + } + switch (realTypeAnnotation.type) { + case 'ReservedTypeAnnotation': + switch (realTypeAnnotation.name) { + case 'RootTag': + return 'return 0.0;'; + default: + realTypeAnnotation.name; + throw new Error(createErrorMessage(realTypeAnnotation.name)); + } + case 'VoidTypeAnnotation': + return ''; + case 'PromiseTypeAnnotation': + return ''; + case 'NumberTypeAnnotation': + return nullable ? 'return null;' : 'return 0;'; + case 'FloatTypeAnnotation': + return nullable ? 'return null;' : 'return 0.0;'; + case 'DoubleTypeAnnotation': + return nullable ? 'return null;' : 'return 0.0;'; + case 'Int32TypeAnnotation': + return nullable ? 'return null;' : 'return 0;'; + case 'BooleanTypeAnnotation': + return nullable ? 'return null;' : 'return false;'; + case 'EnumDeclaration': + switch (realTypeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return nullable ? 'return null;' : 'return 0;'; + case 'StringTypeAnnotation': + return nullable ? 'return null;' : 'return "";'; + default: + throw new Error(createErrorMessage(realTypeAnnotation.type)); + } + case 'UnionTypeAnnotation': + switch (realTypeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return nullable ? 'return null;' : 'return 0;'; + case 'ObjectTypeAnnotation': + return 'return null;'; + case 'StringTypeAnnotation': + return nullable ? 'return null;' : 'return "";'; + default: + throw new Error( + `Unsupported union member returning value, found: ${realTypeAnnotation.memberType}"`, + ); + } + case 'StringTypeAnnotation': + return nullable ? 'return null;' : 'return "";'; + case 'ObjectTypeAnnotation': + return 'return null;'; + case 'GenericObjectTypeAnnotation': + return 'return null;'; + case 'ArrayTypeAnnotation': + return 'return null;'; + default: + realTypeAnnotation.type; + throw new Error(createErrorMessage(realTypeAnnotation.type)); + } +} + +// Build special-cased runtime check for getConstants(). +function buildGetConstantsMethod(method, imports, resolveAlias) { + const _unwrapNullable7 = unwrapNullable(method.typeAnnotation), + _unwrapNullable8 = _slicedToArray(_unwrapNullable7, 1), + methodTypeAnnotation = _unwrapNullable8[0]; + let returnTypeAnnotation = methodTypeAnnotation.returnTypeAnnotation; + if (returnTypeAnnotation.type === 'TypeAliasTypeAnnotation') { + // The return type is an alias, resolve it to get the expected undelying object literal type + returnTypeAnnotation = resolveAlias(returnTypeAnnotation.name); + } + if (returnTypeAnnotation.type === 'ObjectTypeAnnotation') { + const requiredProps = []; + const optionalProps = []; + const rawProperties = returnTypeAnnotation.properties || []; + rawProperties.forEach(p => { + if (p.optional || p.typeAnnotation.type === 'NullableTypeAnnotation') { + optionalProps.push(p.name); + } else { + requiredProps.push(p.name); + } + }); + if (requiredProps.length === 0 && optionalProps.length === 0) { + // Nothing to validate during runtime. + return ''; + } + imports.add('com.facebook.react.common.build.ReactBuildConfig'); + imports.add('java.util.Arrays'); + imports.add('java.util.HashSet'); + imports.add('java.util.Map'); + imports.add('java.util.Set'); + imports.add('javax.annotation.Nullable'); + const requiredPropsFragment = + requiredProps.length > 0 + ? `Arrays.asList( + ${requiredProps + .sort() + .map(p => `"${p}"`) + .join(',\n ')} + )` + : ''; + const optionalPropsFragment = + optionalProps.length > 0 + ? `Arrays.asList( + ${optionalProps + .sort() + .map(p => `"${p}"`) + .join(',\n ')} + )` + : ''; + return ` protected abstract Map getTypedExportedConstants(); + + @Override + @DoNotStrip + public final @Nullable Map getConstants() { + Map constants = getTypedExportedConstants(); + if (ReactBuildConfig.DEBUG || ReactBuildConfig.IS_INTERNAL_BUILD) { + Set obligatoryFlowConstants = new HashSet<>(${requiredPropsFragment}); + Set optionalFlowConstants = new HashSet<>(${optionalPropsFragment}); + Set undeclaredConstants = new HashSet<>(constants.keySet()); + undeclaredConstants.removeAll(obligatoryFlowConstants); + undeclaredConstants.removeAll(optionalFlowConstants); + if (!undeclaredConstants.isEmpty()) { + throw new IllegalStateException(String.format("Native Module Flow doesn't declare constants: %s", undeclaredConstants)); + } + undeclaredConstants = obligatoryFlowConstants; + undeclaredConstants.removeAll(constants.keySet()); + if (!undeclaredConstants.isEmpty()) { + throw new IllegalStateException(String.format("Native Module doesn't fill in constants: %s", undeclaredConstants)); + } + } + return constants; + }`; + } + return ''; +} +module.exports = { + generate( + libraryName, + schema, + packageName, + assumeNonnull = false, + headerPrefix, + ) { + const files = new Map(); + const nativeModules = getModules(schema); + const normalizedPackageName = + packageName == null ? 'com.facebook.fbreact.specs' : packageName; + const outputDir = `java/${normalizedPackageName.replace(/\./g, '/')}`; + Object.keys(nativeModules).forEach(hasteModuleName => { + const _nativeModules$hasteM = nativeModules[hasteModuleName], + aliasMap = _nativeModules$hasteM.aliasMap, + excludedPlatforms = _nativeModules$hasteM.excludedPlatforms, + moduleName = _nativeModules$hasteM.moduleName, + spec = _nativeModules$hasteM.spec; + if (excludedPlatforms != null && excludedPlatforms.includes('android')) { + return; + } + const resolveAlias = createAliasResolver(aliasMap); + const className = `${hasteModuleName}Spec`; + const imports = new Set([ + // Always required. + 'com.facebook.react.bridge.ReactApplicationContext', + 'com.facebook.react.bridge.ReactContextBaseJavaModule', + 'com.facebook.react.bridge.ReactMethod', + 'com.facebook.react.turbomodule.core.interfaces.TurboModule', + 'com.facebook.proguard.annotations.DoNotStrip', + 'javax.annotation.Nonnull', + ]); + const methods = spec.methods.map(method => { + if (method.name === 'getConstants') { + return buildGetConstantsMethod(method, imports, resolveAlias); + } + const _unwrapNullable9 = unwrapNullable(method.typeAnnotation), + _unwrapNullable10 = _slicedToArray(_unwrapNullable9, 1), + methodTypeAnnotation = _unwrapNullable10[0]; + + // Handle return type + const translatedReturnType = translateFunctionReturnTypeToJavaType( + methodTypeAnnotation.returnTypeAnnotation, + typeName => + `Unsupported return type for method ${method.name}. Found: ${typeName}`, + resolveAlias, + imports, + ); + const returningPromise = + methodTypeAnnotation.returnTypeAnnotation.type === + 'PromiseTypeAnnotation'; + const isSyncMethod = + methodTypeAnnotation.returnTypeAnnotation.type !== + 'VoidTypeAnnotation' && !returningPromise; + + // Handle method args + const traversedArgs = methodTypeAnnotation.params.map(param => { + const translatedParam = translateFunctionParamToJavaType( + param, + typeName => + `Unsupported type for param "${param.name}" in ${method.name}. Found: ${typeName}`, + resolveAlias, + imports, + ); + return `${translatedParam} ${param.name}`; + }); + if (returningPromise) { + // Promise return type requires an extra arg at the end. + imports.add('com.facebook.react.bridge.Promise'); + traversedArgs.push('Promise promise'); + } + const methodJavaAnnotation = `@ReactMethod${ + isSyncMethod ? '(isBlockingSynchronousMethod = true)' : '' + }\n @DoNotStrip`; + const methodBody = method.optional + ? getFalsyReturnStatementFromReturnType( + methodTypeAnnotation.returnTypeAnnotation, + typeName => + `Cannot build falsy return statement for return type for method ${method.name}. Found: ${typeName}`, + resolveAlias, + ) + : null; + return MethodTemplate({ + abstract: !method.optional, + methodBody, + methodJavaAnnotation, + methodName: method.name, + translatedReturnType, + traversedArgs, + }); + }); + files.set( + `${outputDir}/${className}.java`, + FileTemplate({ + packageName: normalizedPackageName, + className, + jsName: moduleName, + methods: methods.filter(Boolean).join('\n\n'), + imports: Array.from(imports) + .sort() + .map(p => `import ${p};`) + .join('\n'), + }), + ); + }); + return files; + }, +}; diff --git a/packages/react-native-codegen/lib/generators/modules/GenerateModuleJavaSpec.js.flow b/packages/react-native-codegen/lib/generators/modules/GenerateModuleJavaSpec.js.flow new file mode 100644 index 00000000000000..ee3237d5d8c0cf --- /dev/null +++ b/packages/react-native-codegen/lib/generators/modules/GenerateModuleJavaSpec.js.flow @@ -0,0 +1,547 @@ +/** + * 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 { + NamedShape, + NativeModuleFunctionTypeAnnotation, + NativeModuleParamTypeAnnotation, + NativeModulePropertyShape, + NativeModuleReturnTypeAnnotation, + Nullable, + SchemaType, +} from '../../CodegenSchema'; +import type {AliasResolver} from './Utils'; + +const {unwrapNullable} = require('../../parsers/parsers-commons'); +const {wrapOptional} = require('../TypeUtils/Java'); +const {createAliasResolver, getModules} = require('./Utils'); + +type FilesOutput = Map; + +function FileTemplate( + config: $ReadOnly<{ + packageName: string, + className: string, + jsName: string, + methods: string, + imports: string, + }>, +): string { + const {packageName, className, jsName, methods, imports} = config; + return ` +/** + * 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: GenerateModuleJavaSpec.js + * + * ${'@'}nolint + */ + +package ${packageName}; + +${imports} + +public abstract class ${className} extends ReactContextBaseJavaModule implements TurboModule { + public static final String NAME = "${jsName}"; + + public ${className}(ReactApplicationContext reactContext) { + super(reactContext); + } + + @Override + public @Nonnull String getName() { + return NAME; + } + +${methods} +} +`; +} + +function MethodTemplate( + config: $ReadOnly<{ + abstract: boolean, + methodBody: ?string, + methodJavaAnnotation: string, + methodName: string, + translatedReturnType: string, + traversedArgs: Array, + }>, +): string { + const { + abstract, + methodBody, + methodJavaAnnotation, + methodName, + translatedReturnType, + traversedArgs, + } = config; + const methodQualifier = abstract ? 'abstract ' : ''; + const methodClosing = abstract + ? ';' + : methodBody != null && methodBody.length > 0 + ? ` { ${methodBody} }` + : ' {}'; + return ` ${methodJavaAnnotation} + public ${methodQualifier}${translatedReturnType} ${methodName}(${traversedArgs.join( + ', ', + )})${methodClosing}`; +} + +type Param = NamedShape>; + +function translateFunctionParamToJavaType( + param: Param, + createErrorMessage: (typeName: string) => string, + resolveAlias: AliasResolver, + imports: Set, +): string { + const {optional, typeAnnotation: nullableTypeAnnotation} = param; + const [typeAnnotation, nullable] = + unwrapNullable(nullableTypeAnnotation); + const isRequired = !optional && !nullable; + if (!isRequired) { + imports.add('javax.annotation.Nullable'); + } + + // FIXME: support class alias for args + let realTypeAnnotation = typeAnnotation; + if (realTypeAnnotation.type === 'TypeAliasTypeAnnotation') { + realTypeAnnotation = resolveAlias(realTypeAnnotation.name); + } + + switch (realTypeAnnotation.type) { + case 'ReservedTypeAnnotation': + switch (realTypeAnnotation.name) { + case 'RootTag': + return wrapOptional('double', isRequired); + default: + (realTypeAnnotation.name: empty); + throw new Error(createErrorMessage(realTypeAnnotation.name)); + } + case 'StringTypeAnnotation': + return wrapOptional('String', isRequired); + case 'NumberTypeAnnotation': + return wrapOptional('double', isRequired); + case 'FloatTypeAnnotation': + return wrapOptional('double', isRequired); + case 'DoubleTypeAnnotation': + return wrapOptional('double', isRequired); + case 'Int32TypeAnnotation': + return wrapOptional('double', isRequired); + case 'BooleanTypeAnnotation': + return wrapOptional('boolean', isRequired); + case 'EnumDeclaration': + switch (realTypeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return wrapOptional('double', isRequired); + case 'StringTypeAnnotation': + return wrapOptional('String', isRequired); + default: + throw new Error(createErrorMessage(realTypeAnnotation.type)); + } + case 'UnionTypeAnnotation': + switch (typeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return wrapOptional('double', isRequired); + case 'ObjectTypeAnnotation': + imports.add('com.facebook.react.bridge.ReadableMap'); + return wrapOptional('ReadableMap', isRequired); + case 'StringTypeAnnotation': + return wrapOptional('String', isRequired); + default: + throw new Error( + `Unsupported union member returning value, found: ${realTypeAnnotation.memberType}"`, + ); + } + case 'ObjectTypeAnnotation': + imports.add('com.facebook.react.bridge.ReadableMap'); + return wrapOptional('ReadableMap', isRequired); + case 'GenericObjectTypeAnnotation': + // Treat this the same as ObjectTypeAnnotation for now. + imports.add('com.facebook.react.bridge.ReadableMap'); + return wrapOptional('ReadableMap', isRequired); + case 'ArrayTypeAnnotation': + imports.add('com.facebook.react.bridge.ReadableArray'); + return wrapOptional('ReadableArray', isRequired); + case 'FunctionTypeAnnotation': + imports.add('com.facebook.react.bridge.Callback'); + return wrapOptional('Callback', isRequired); + default: + (realTypeAnnotation.type: 'MixedTypeAnnotation'); + throw new Error(createErrorMessage(realTypeAnnotation.type)); + } +} + +function translateFunctionReturnTypeToJavaType( + nullableReturnTypeAnnotation: Nullable, + createErrorMessage: (typeName: string) => string, + resolveAlias: AliasResolver, + imports: Set, +): string { + const [returnTypeAnnotation, nullable] = + unwrapNullable( + nullableReturnTypeAnnotation, + ); + + if (nullable) { + imports.add('javax.annotation.Nullable'); + } + + const isRequired = !nullable; + + // FIXME: support class alias for args + let realTypeAnnotation = returnTypeAnnotation; + if (realTypeAnnotation.type === 'TypeAliasTypeAnnotation') { + realTypeAnnotation = resolveAlias(realTypeAnnotation.name); + } + + switch (realTypeAnnotation.type) { + case 'ReservedTypeAnnotation': + switch (realTypeAnnotation.name) { + case 'RootTag': + return wrapOptional('double', isRequired); + default: + (realTypeAnnotation.name: empty); + throw new Error(createErrorMessage(realTypeAnnotation.name)); + } + case 'VoidTypeAnnotation': + return 'void'; + case 'PromiseTypeAnnotation': + return 'void'; + case 'StringTypeAnnotation': + return wrapOptional('String', isRequired); + case 'NumberTypeAnnotation': + return wrapOptional('double', isRequired); + case 'FloatTypeAnnotation': + return wrapOptional('double', isRequired); + case 'DoubleTypeAnnotation': + return wrapOptional('double', isRequired); + case 'Int32TypeAnnotation': + return wrapOptional('double', isRequired); + case 'BooleanTypeAnnotation': + return wrapOptional('boolean', isRequired); + case 'EnumDeclaration': + switch (realTypeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return wrapOptional('double', isRequired); + case 'StringTypeAnnotation': + return wrapOptional('String', isRequired); + default: + throw new Error(createErrorMessage(realTypeAnnotation.type)); + } + case 'UnionTypeAnnotation': + switch (realTypeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return wrapOptional('double', isRequired); + case 'ObjectTypeAnnotation': + imports.add('com.facebook.react.bridge.WritableMap'); + return wrapOptional('WritableMap', isRequired); + case 'StringTypeAnnotation': + return wrapOptional('String', isRequired); + default: + throw new Error( + `Unsupported union member returning value, found: ${realTypeAnnotation.memberType}"`, + ); + } + case 'ObjectTypeAnnotation': + imports.add('com.facebook.react.bridge.WritableMap'); + return wrapOptional('WritableMap', isRequired); + case 'GenericObjectTypeAnnotation': + imports.add('com.facebook.react.bridge.WritableMap'); + return wrapOptional('WritableMap', isRequired); + case 'ArrayTypeAnnotation': + imports.add('com.facebook.react.bridge.WritableArray'); + return wrapOptional('WritableArray', isRequired); + default: + (realTypeAnnotation.type: 'MixedTypeAnnotation'); + throw new Error(createErrorMessage(realTypeAnnotation.type)); + } +} + +function getFalsyReturnStatementFromReturnType( + nullableReturnTypeAnnotation: Nullable, + createErrorMessage: (typeName: string) => string, + resolveAlias: AliasResolver, +): string { + const [returnTypeAnnotation, nullable] = + unwrapNullable( + nullableReturnTypeAnnotation, + ); + + let realTypeAnnotation = returnTypeAnnotation; + if (realTypeAnnotation.type === 'TypeAliasTypeAnnotation') { + realTypeAnnotation = resolveAlias(realTypeAnnotation.name); + } + + switch (realTypeAnnotation.type) { + case 'ReservedTypeAnnotation': + switch (realTypeAnnotation.name) { + case 'RootTag': + return 'return 0.0;'; + default: + (realTypeAnnotation.name: empty); + throw new Error(createErrorMessage(realTypeAnnotation.name)); + } + case 'VoidTypeAnnotation': + return ''; + case 'PromiseTypeAnnotation': + return ''; + case 'NumberTypeAnnotation': + return nullable ? 'return null;' : 'return 0;'; + case 'FloatTypeAnnotation': + return nullable ? 'return null;' : 'return 0.0;'; + case 'DoubleTypeAnnotation': + return nullable ? 'return null;' : 'return 0.0;'; + case 'Int32TypeAnnotation': + return nullable ? 'return null;' : 'return 0;'; + case 'BooleanTypeAnnotation': + return nullable ? 'return null;' : 'return false;'; + case 'EnumDeclaration': + switch (realTypeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return nullable ? 'return null;' : 'return 0;'; + case 'StringTypeAnnotation': + return nullable ? 'return null;' : 'return "";'; + default: + throw new Error(createErrorMessage(realTypeAnnotation.type)); + } + case 'UnionTypeAnnotation': + switch (realTypeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return nullable ? 'return null;' : 'return 0;'; + case 'ObjectTypeAnnotation': + return 'return null;'; + case 'StringTypeAnnotation': + return nullable ? 'return null;' : 'return "";'; + default: + throw new Error( + `Unsupported union member returning value, found: ${realTypeAnnotation.memberType}"`, + ); + } + case 'StringTypeAnnotation': + return nullable ? 'return null;' : 'return "";'; + case 'ObjectTypeAnnotation': + return 'return null;'; + case 'GenericObjectTypeAnnotation': + return 'return null;'; + case 'ArrayTypeAnnotation': + return 'return null;'; + default: + (realTypeAnnotation.type: 'MixedTypeAnnotation'); + throw new Error(createErrorMessage(realTypeAnnotation.type)); + } +} + +// Build special-cased runtime check for getConstants(). +function buildGetConstantsMethod( + method: NativeModulePropertyShape, + imports: Set, + resolveAlias: AliasResolver, +): string { + const [methodTypeAnnotation] = + unwrapNullable(method.typeAnnotation); + let returnTypeAnnotation = methodTypeAnnotation.returnTypeAnnotation; + if (returnTypeAnnotation.type === 'TypeAliasTypeAnnotation') { + // The return type is an alias, resolve it to get the expected undelying object literal type + returnTypeAnnotation = resolveAlias(returnTypeAnnotation.name); + } + + if (returnTypeAnnotation.type === 'ObjectTypeAnnotation') { + const requiredProps = []; + const optionalProps = []; + const rawProperties = returnTypeAnnotation.properties || []; + rawProperties.forEach(p => { + if (p.optional || p.typeAnnotation.type === 'NullableTypeAnnotation') { + optionalProps.push(p.name); + } else { + requiredProps.push(p.name); + } + }); + if (requiredProps.length === 0 && optionalProps.length === 0) { + // Nothing to validate during runtime. + return ''; + } + + imports.add('com.facebook.react.common.build.ReactBuildConfig'); + imports.add('java.util.Arrays'); + imports.add('java.util.HashSet'); + imports.add('java.util.Map'); + imports.add('java.util.Set'); + imports.add('javax.annotation.Nullable'); + + const requiredPropsFragment = + requiredProps.length > 0 + ? `Arrays.asList( + ${requiredProps + .sort() + .map(p => `"${p}"`) + .join(',\n ')} + )` + : ''; + const optionalPropsFragment = + optionalProps.length > 0 + ? `Arrays.asList( + ${optionalProps + .sort() + .map(p => `"${p}"`) + .join(',\n ')} + )` + : ''; + + return ` protected abstract Map getTypedExportedConstants(); + + @Override + @DoNotStrip + public final @Nullable Map getConstants() { + Map constants = getTypedExportedConstants(); + if (ReactBuildConfig.DEBUG || ReactBuildConfig.IS_INTERNAL_BUILD) { + Set obligatoryFlowConstants = new HashSet<>(${requiredPropsFragment}); + Set optionalFlowConstants = new HashSet<>(${optionalPropsFragment}); + Set undeclaredConstants = new HashSet<>(constants.keySet()); + undeclaredConstants.removeAll(obligatoryFlowConstants); + undeclaredConstants.removeAll(optionalFlowConstants); + if (!undeclaredConstants.isEmpty()) { + throw new IllegalStateException(String.format("Native Module Flow doesn't declare constants: %s", undeclaredConstants)); + } + undeclaredConstants = obligatoryFlowConstants; + undeclaredConstants.removeAll(constants.keySet()); + if (!undeclaredConstants.isEmpty()) { + throw new IllegalStateException(String.format("Native Module doesn't fill in constants: %s", undeclaredConstants)); + } + } + return constants; + }`; + } + + return ''; +} + +module.exports = { + generate( + libraryName: string, + schema: SchemaType, + packageName?: string, + assumeNonnull: boolean = false, + headerPrefix?: string, + ): FilesOutput { + const files = new Map(); + const nativeModules = getModules(schema); + + const normalizedPackageName = + packageName == null ? 'com.facebook.fbreact.specs' : packageName; + const outputDir = `java/${normalizedPackageName.replace(/\./g, '/')}`; + + Object.keys(nativeModules).forEach(hasteModuleName => { + const {aliasMap, excludedPlatforms, moduleName, spec} = + nativeModules[hasteModuleName]; + if (excludedPlatforms != null && excludedPlatforms.includes('android')) { + return; + } + const resolveAlias = createAliasResolver(aliasMap); + const className = `${hasteModuleName}Spec`; + + const imports: Set = new Set([ + // Always required. + 'com.facebook.react.bridge.ReactApplicationContext', + 'com.facebook.react.bridge.ReactContextBaseJavaModule', + 'com.facebook.react.bridge.ReactMethod', + 'com.facebook.react.turbomodule.core.interfaces.TurboModule', + 'com.facebook.proguard.annotations.DoNotStrip', + 'javax.annotation.Nonnull', + ]); + + const methods = spec.methods.map(method => { + if (method.name === 'getConstants') { + return buildGetConstantsMethod(method, imports, resolveAlias); + } + + const [methodTypeAnnotation] = + unwrapNullable( + method.typeAnnotation, + ); + + // Handle return type + const translatedReturnType = translateFunctionReturnTypeToJavaType( + methodTypeAnnotation.returnTypeAnnotation, + typeName => + `Unsupported return type for method ${method.name}. Found: ${typeName}`, + resolveAlias, + imports, + ); + const returningPromise = + methodTypeAnnotation.returnTypeAnnotation.type === + 'PromiseTypeAnnotation'; + const isSyncMethod = + methodTypeAnnotation.returnTypeAnnotation.type !== + 'VoidTypeAnnotation' && !returningPromise; + + // Handle method args + const traversedArgs = methodTypeAnnotation.params.map(param => { + const translatedParam = translateFunctionParamToJavaType( + param, + typeName => + `Unsupported type for param "${param.name}" in ${method.name}. Found: ${typeName}`, + resolveAlias, + imports, + ); + return `${translatedParam} ${param.name}`; + }); + + if (returningPromise) { + // Promise return type requires an extra arg at the end. + imports.add('com.facebook.react.bridge.Promise'); + traversedArgs.push('Promise promise'); + } + + const methodJavaAnnotation = `@ReactMethod${ + isSyncMethod ? '(isBlockingSynchronousMethod = true)' : '' + }\n @DoNotStrip`; + const methodBody = method.optional + ? getFalsyReturnStatementFromReturnType( + methodTypeAnnotation.returnTypeAnnotation, + typeName => + `Cannot build falsy return statement for return type for method ${method.name}. Found: ${typeName}`, + resolveAlias, + ) + : null; + return MethodTemplate({ + abstract: !method.optional, + methodBody, + methodJavaAnnotation, + methodName: method.name, + translatedReturnType, + traversedArgs, + }); + }); + + files.set( + `${outputDir}/${className}.java`, + FileTemplate({ + packageName: normalizedPackageName, + className, + jsName: moduleName, + methods: methods.filter(Boolean).join('\n\n'), + imports: Array.from(imports) + .sort() + .map(p => `import ${p};`) + .join('\n'), + }), + ); + }); + + return files; + }, +}; diff --git a/packages/react-native-codegen/lib/generators/modules/GenerateModuleJniCpp.js b/packages/react-native-codegen/lib/generators/modules/GenerateModuleJniCpp.js new file mode 100644 index 00000000000000..e28bc35909dbfe --- /dev/null +++ b/packages/react-native-codegen/lib/generators/modules/GenerateModuleJniCpp.js @@ -0,0 +1,513 @@ +/** + * 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'; + +function _slicedToArray(r, e) { + return ( + _arrayWithHoles(r) || + _iterableToArrayLimit(r, e) || + _unsupportedIterableToArray(r, e) || + _nonIterableRest() + ); +} +function _nonIterableRest() { + throw new TypeError( + 'Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.', + ); +} +function _unsupportedIterableToArray(r, a) { + if (r) { + if ('string' == typeof r) return _arrayLikeToArray(r, a); + var t = {}.toString.call(r).slice(8, -1); + return ( + 'Object' === t && r.constructor && (t = r.constructor.name), + 'Map' === t || 'Set' === t + ? Array.from(r) + : 'Arguments' === t || + /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) + ? _arrayLikeToArray(r, a) + : void 0 + ); + } +} +function _arrayLikeToArray(r, a) { + (null == a || a > r.length) && (a = r.length); + for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; + return n; +} +function _iterableToArrayLimit(r, l) { + var t = + null == r + ? null + : ('undefined' != typeof Symbol && r[Symbol.iterator]) || r['@@iterator']; + if (null != t) { + var e, + n, + i, + u, + a = [], + f = !0, + o = !1; + try { + if (((i = (t = t.call(r)).next), 0 === l)) { + if (Object(t) !== t) return; + f = !1; + } else + for ( + ; + !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); + f = !0 + ); + } catch (r) { + (o = !0), (n = r); + } finally { + try { + if (!f && null != t.return && ((u = t.return()), Object(u) !== u)) + return; + } finally { + if (o) throw n; + } + } + return a; + } +} +function _arrayWithHoles(r) { + if (Array.isArray(r)) return r; +} +const _require = require('../../parsers/parsers-commons'), + unwrapNullable = _require.unwrapNullable; +const _require2 = require('./Utils'), + createAliasResolver = _require2.createAliasResolver, + getModules = _require2.getModules; +const HostFunctionTemplate = ({ + hasteModuleName, + propertyName, + jniSignature, + jsReturnType, +}) => { + return `static facebook::jsi::Value __hostFunction_${hasteModuleName}SpecJSI_${propertyName}(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + static jmethodID cachedMethodId = nullptr; + return static_cast(turboModule).invokeJavaMethod(rt, ${jsReturnType}, "${propertyName}", "${jniSignature}", args, count, cachedMethodId); +}`; +}; +const ModuleClassConstructorTemplate = ({hasteModuleName, methods}) => { + return ` +${hasteModuleName}SpecJSI::${hasteModuleName}SpecJSI(const JavaTurboModule::InitParams ¶ms) + : JavaTurboModule(params) { +${methods + .map(({propertyName, argCount}) => { + return ` methodMap_["${propertyName}"] = MethodMetadata {${argCount}, __hostFunction_${hasteModuleName}SpecJSI_${propertyName}};`; + }) + .join('\n')} +}`.trim(); +}; +const ModuleLookupTemplate = ({moduleName, hasteModuleName}) => { + return ` if (moduleName == "${moduleName}") { + return std::make_shared<${hasteModuleName}SpecJSI>(params); + }`; +}; +const FileTemplate = ({libraryName, include, modules, moduleLookups}) => { + return ` +/** + * 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: GenerateModuleJniCpp.js + */ + +#include ${include} + +namespace facebook::react { + +${modules} + +std::shared_ptr ${libraryName}_ModuleProvider(const std::string &moduleName, const JavaTurboModule::InitParams ¶ms) { +${moduleLookups.map(ModuleLookupTemplate).join('\n')} + return nullptr; +} + +} // namespace facebook::react +`; +}; +function translateReturnTypeToKind(nullableTypeAnnotation, resolveAlias) { + const _unwrapNullable = unwrapNullable(nullableTypeAnnotation), + _unwrapNullable2 = _slicedToArray(_unwrapNullable, 1), + typeAnnotation = _unwrapNullable2[0]; + let realTypeAnnotation = typeAnnotation; + if (realTypeAnnotation.type === 'TypeAliasTypeAnnotation') { + realTypeAnnotation = resolveAlias(realTypeAnnotation.name); + } + switch (realTypeAnnotation.type) { + case 'ReservedTypeAnnotation': + switch (realTypeAnnotation.name) { + case 'RootTag': + return 'NumberKind'; + default: + realTypeAnnotation.name; + throw new Error( + `Invalid ReservedFunctionValueTypeName name, got ${realTypeAnnotation.name}`, + ); + } + case 'VoidTypeAnnotation': + return 'VoidKind'; + case 'StringTypeAnnotation': + return 'StringKind'; + case 'BooleanTypeAnnotation': + return 'BooleanKind'; + case 'EnumDeclaration': + switch (typeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return 'NumberKind'; + case 'StringTypeAnnotation': + return 'StringKind'; + default: + throw new Error( + `Unknown enum prop type for returning value, found: ${realTypeAnnotation.type}"`, + ); + } + case 'UnionTypeAnnotation': + switch (typeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return 'NumberKind'; + case 'ObjectTypeAnnotation': + return 'ObjectKind'; + case 'StringTypeAnnotation': + return 'StringKind'; + default: + throw new Error( + `Unsupported union member returning value, found: ${realTypeAnnotation.memberType}"`, + ); + } + case 'NumberTypeAnnotation': + return 'NumberKind'; + case 'DoubleTypeAnnotation': + return 'NumberKind'; + case 'FloatTypeAnnotation': + return 'NumberKind'; + case 'Int32TypeAnnotation': + return 'NumberKind'; + case 'PromiseTypeAnnotation': + return 'PromiseKind'; + case 'GenericObjectTypeAnnotation': + return 'ObjectKind'; + case 'ObjectTypeAnnotation': + return 'ObjectKind'; + case 'ArrayTypeAnnotation': + return 'ArrayKind'; + default: + realTypeAnnotation.type; + throw new Error( + `Unknown prop type for returning value, found: ${realTypeAnnotation.type}"`, + ); + } +} +function translateParamTypeToJniType(param, resolveAlias) { + const optional = param.optional, + nullableTypeAnnotation = param.typeAnnotation; + const _unwrapNullable3 = unwrapNullable(nullableTypeAnnotation), + _unwrapNullable4 = _slicedToArray(_unwrapNullable3, 2), + typeAnnotation = _unwrapNullable4[0], + nullable = _unwrapNullable4[1]; + const isRequired = !optional && !nullable; + let realTypeAnnotation = typeAnnotation; + if (realTypeAnnotation.type === 'TypeAliasTypeAnnotation') { + realTypeAnnotation = resolveAlias(realTypeAnnotation.name); + } + switch (realTypeAnnotation.type) { + case 'ReservedTypeAnnotation': + switch (realTypeAnnotation.name) { + case 'RootTag': + return !isRequired ? 'Ljava/lang/Double;' : 'D'; + default: + realTypeAnnotation.name; + throw new Error( + `Invalid ReservedFunctionValueTypeName name, got ${realTypeAnnotation.name}`, + ); + } + case 'StringTypeAnnotation': + return 'Ljava/lang/String;'; + case 'BooleanTypeAnnotation': + return !isRequired ? 'Ljava/lang/Boolean;' : 'Z'; + case 'EnumDeclaration': + switch (typeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return !isRequired ? 'Ljava/lang/Double;' : 'D'; + case 'StringTypeAnnotation': + return 'Ljava/lang/String;'; + default: + throw new Error( + `Unknown enum prop type for method arg, found: ${realTypeAnnotation.type}"`, + ); + } + case 'UnionTypeAnnotation': + switch (typeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return !isRequired ? 'Ljava/lang/Double;' : 'D'; + case 'ObjectTypeAnnotation': + return 'Lcom/facebook/react/bridge/ReadableMap;'; + case 'StringTypeAnnotation': + return 'Ljava/lang/String;'; + default: + throw new Error( + `Unsupported union prop value, found: ${realTypeAnnotation.memberType}"`, + ); + } + case 'NumberTypeAnnotation': + return !isRequired ? 'Ljava/lang/Double;' : 'D'; + case 'DoubleTypeAnnotation': + return !isRequired ? 'Ljava/lang/Double;' : 'D'; + case 'FloatTypeAnnotation': + return !isRequired ? 'Ljava/lang/Double;' : 'D'; + case 'Int32TypeAnnotation': + return !isRequired ? 'Ljava/lang/Double;' : 'D'; + case 'GenericObjectTypeAnnotation': + return 'Lcom/facebook/react/bridge/ReadableMap;'; + case 'ObjectTypeAnnotation': + return 'Lcom/facebook/react/bridge/ReadableMap;'; + case 'ArrayTypeAnnotation': + return 'Lcom/facebook/react/bridge/ReadableArray;'; + case 'FunctionTypeAnnotation': + return 'Lcom/facebook/react/bridge/Callback;'; + default: + realTypeAnnotation.type; + throw new Error( + `Unknown prop type for method arg, found: ${realTypeAnnotation.type}"`, + ); + } +} +function translateReturnTypeToJniType(nullableTypeAnnotation, resolveAlias) { + const _unwrapNullable5 = unwrapNullable(nullableTypeAnnotation), + _unwrapNullable6 = _slicedToArray(_unwrapNullable5, 2), + typeAnnotation = _unwrapNullable6[0], + nullable = _unwrapNullable6[1]; + let realTypeAnnotation = typeAnnotation; + if (realTypeAnnotation.type === 'TypeAliasTypeAnnotation') { + realTypeAnnotation = resolveAlias(realTypeAnnotation.name); + } + switch (realTypeAnnotation.type) { + case 'ReservedTypeAnnotation': + switch (realTypeAnnotation.name) { + case 'RootTag': + return nullable ? 'Ljava/lang/Double;' : 'D'; + default: + realTypeAnnotation.name; + throw new Error( + `Invalid ReservedFunctionValueTypeName name, got ${realTypeAnnotation.name}`, + ); + } + case 'VoidTypeAnnotation': + return 'V'; + case 'StringTypeAnnotation': + return 'Ljava/lang/String;'; + case 'BooleanTypeAnnotation': + return nullable ? 'Ljava/lang/Boolean;' : 'Z'; + case 'EnumDeclaration': + switch (typeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return nullable ? 'Ljava/lang/Double;' : 'D'; + case 'StringTypeAnnotation': + return 'Ljava/lang/String;'; + default: + throw new Error( + `Unknown enum prop type for method return type, found: ${realTypeAnnotation.type}"`, + ); + } + case 'UnionTypeAnnotation': + switch (typeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return nullable ? 'Ljava/lang/Double;' : 'D'; + case 'ObjectTypeAnnotation': + return 'Lcom/facebook/react/bridge/WritableMap;'; + case 'StringTypeAnnotation': + return 'Ljava/lang/String;'; + default: + throw new Error( + `Unsupported union member type, found: ${realTypeAnnotation.memberType}"`, + ); + } + case 'NumberTypeAnnotation': + return nullable ? 'Ljava/lang/Double;' : 'D'; + case 'DoubleTypeAnnotation': + return nullable ? 'Ljava/lang/Double;' : 'D'; + case 'FloatTypeAnnotation': + return nullable ? 'Ljava/lang/Double;' : 'D'; + case 'Int32TypeAnnotation': + return nullable ? 'Ljava/lang/Double;' : 'D'; + case 'PromiseTypeAnnotation': + return 'Lcom/facebook/react/bridge/Promise;'; + case 'GenericObjectTypeAnnotation': + return 'Lcom/facebook/react/bridge/WritableMap;'; + case 'ObjectTypeAnnotation': + return 'Lcom/facebook/react/bridge/WritableMap;'; + case 'ArrayTypeAnnotation': + return 'Lcom/facebook/react/bridge/WritableArray;'; + default: + realTypeAnnotation.type; + throw new Error( + `Unknown prop type for method return type, found: ${realTypeAnnotation.type}"`, + ); + } +} +function translateMethodTypeToJniSignature(property, resolveAlias) { + const name = property.name, + typeAnnotation = property.typeAnnotation; + let _unwrapNullable7 = unwrapNullable(typeAnnotation), + _unwrapNullable8 = _slicedToArray(_unwrapNullable7, 1), + _unwrapNullable8$ = _unwrapNullable8[0], + returnTypeAnnotation = _unwrapNullable8$.returnTypeAnnotation, + params = _unwrapNullable8$.params; + params = [...params]; + let processedReturnTypeAnnotation = returnTypeAnnotation; + const isPromiseReturn = returnTypeAnnotation.type === 'PromiseTypeAnnotation'; + if (isPromiseReturn) { + processedReturnTypeAnnotation = { + type: 'VoidTypeAnnotation', + }; + } + const argsSignatureParts = params.map(t => { + return translateParamTypeToJniType(t, resolveAlias); + }); + if (isPromiseReturn) { + // Additional promise arg for this case. + argsSignatureParts.push( + translateReturnTypeToJniType(returnTypeAnnotation, resolveAlias), + ); + } + const argsSignature = argsSignatureParts.join(''); + const returnSignature = + name === 'getConstants' + ? 'Ljava/util/Map;' + : translateReturnTypeToJniType( + processedReturnTypeAnnotation, + resolveAlias, + ); + return `(${argsSignature})${returnSignature}`; +} +function translateMethodForImplementation( + hasteModuleName, + property, + resolveAlias, +) { + const _unwrapNullable9 = unwrapNullable(property.typeAnnotation), + _unwrapNullable10 = _slicedToArray(_unwrapNullable9, 1), + propertyTypeAnnotation = _unwrapNullable10[0]; + const returnTypeAnnotation = propertyTypeAnnotation.returnTypeAnnotation; + if ( + property.name === 'getConstants' && + returnTypeAnnotation.type === 'ObjectTypeAnnotation' && + returnTypeAnnotation.properties.length === 0 + ) { + return ''; + } + return HostFunctionTemplate({ + hasteModuleName, + propertyName: property.name, + jniSignature: translateMethodTypeToJniSignature(property, resolveAlias), + jsReturnType: translateReturnTypeToKind(returnTypeAnnotation, resolveAlias), + }); +} +module.exports = { + generate( + libraryName, + schema, + packageName, + assumeNonnull = false, + headerPrefix, + ) { + const nativeModules = getModules(schema); + const modules = Object.keys(nativeModules) + .filter(hasteModuleName => { + const module = nativeModules[hasteModuleName]; + return !( + module.excludedPlatforms != null && + module.excludedPlatforms.includes('android') + ); + }) + .sort() + .map(hasteModuleName => { + const _nativeModules$hasteM = nativeModules[hasteModuleName], + aliasMap = _nativeModules$hasteM.aliasMap, + methods = _nativeModules$hasteM.spec.methods; + const resolveAlias = createAliasResolver(aliasMap); + const translatedMethods = methods + .map(property => + translateMethodForImplementation( + hasteModuleName, + property, + resolveAlias, + ), + ) + .join('\n\n'); + return ( + translatedMethods + + '\n\n' + + ModuleClassConstructorTemplate({ + hasteModuleName, + methods: methods + .map(({name: propertyName, typeAnnotation}) => { + const _unwrapNullable11 = unwrapNullable(typeAnnotation), + _unwrapNullable12 = _slicedToArray(_unwrapNullable11, 1), + _unwrapNullable12$ = _unwrapNullable12[0], + returnTypeAnnotation = + _unwrapNullable12$.returnTypeAnnotation, + params = _unwrapNullable12$.params; + if ( + propertyName === 'getConstants' && + returnTypeAnnotation.type === 'ObjectTypeAnnotation' && + returnTypeAnnotation.properties && + returnTypeAnnotation.properties.length === 0 + ) { + return null; + } + return { + propertyName, + argCount: params.length, + }; + }) + .filter(Boolean), + }) + ); + }) + .join('\n'); + const moduleLookups = Object.keys(nativeModules) + .filter(hasteModuleName => { + const module = nativeModules[hasteModuleName]; + return !( + module.excludedPlatforms != null && + module.excludedPlatforms.includes('android') + ); + }) + .sort((a, b) => { + const nameA = nativeModules[a].moduleName; + const nameB = nativeModules[b].moduleName; + if (nameA < nameB) { + return -1; + } else if (nameA > nameB) { + return 1; + } + return 0; + }) + .map(hasteModuleName => ({ + moduleName: nativeModules[hasteModuleName].moduleName, + hasteModuleName, + })); + const fileName = `${libraryName}-generated.cpp`; + const replacedTemplate = FileTemplate({ + modules: modules, + libraryName: libraryName.replace(/-/g, '_'), + moduleLookups, + include: `"${libraryName}.h"`, + }); + return new Map([[`jni/${fileName}`, replacedTemplate]]); + }, +}; diff --git a/packages/react-native-codegen/lib/generators/modules/GenerateModuleJniCpp.js.flow b/packages/react-native-codegen/lib/generators/modules/GenerateModuleJniCpp.js.flow new file mode 100644 index 00000000000000..2930ab1be9a0be --- /dev/null +++ b/packages/react-native-codegen/lib/generators/modules/GenerateModuleJniCpp.js.flow @@ -0,0 +1,522 @@ +/** + * 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 { + NamedShape, + NativeModuleFunctionTypeAnnotation, + NativeModuleParamTypeAnnotation, + NativeModulePropertyShape, + NativeModuleReturnTypeAnnotation, + Nullable, + SchemaType, +} from '../../CodegenSchema'; +import type {AliasResolver} from './Utils'; + +const {unwrapNullable} = require('../../parsers/parsers-commons'); +const {createAliasResolver, getModules} = require('./Utils'); + +type FilesOutput = Map; + +type JSReturnType = + | 'VoidKind' + | 'StringKind' + | 'BooleanKind' + | 'NumberKind' + | 'PromiseKind' + | 'ObjectKind' + | 'ArrayKind'; + +const HostFunctionTemplate = ({ + hasteModuleName, + propertyName, + jniSignature, + jsReturnType, +}: $ReadOnly<{ + hasteModuleName: string, + propertyName: string, + jniSignature: string, + jsReturnType: JSReturnType, +}>) => { + return `static facebook::jsi::Value __hostFunction_${hasteModuleName}SpecJSI_${propertyName}(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + static jmethodID cachedMethodId = nullptr; + return static_cast(turboModule).invokeJavaMethod(rt, ${jsReturnType}, "${propertyName}", "${jniSignature}", args, count, cachedMethodId); +}`; +}; + +const ModuleClassConstructorTemplate = ({ + hasteModuleName, + methods, +}: $ReadOnly<{ + hasteModuleName: string, + methods: $ReadOnlyArray<{ + propertyName: string, + argCount: number, + }>, +}>) => { + return ` +${hasteModuleName}SpecJSI::${hasteModuleName}SpecJSI(const JavaTurboModule::InitParams ¶ms) + : JavaTurboModule(params) { +${methods + .map(({propertyName, argCount}) => { + return ` methodMap_["${propertyName}"] = MethodMetadata {${argCount}, __hostFunction_${hasteModuleName}SpecJSI_${propertyName}};`; + }) + .join('\n')} +}`.trim(); +}; + +const ModuleLookupTemplate = ({ + moduleName, + hasteModuleName, +}: $ReadOnly<{moduleName: string, hasteModuleName: string}>) => { + return ` if (moduleName == "${moduleName}") { + return std::make_shared<${hasteModuleName}SpecJSI>(params); + }`; +}; + +const FileTemplate = ({ + libraryName, + include, + modules, + moduleLookups, +}: $ReadOnly<{ + libraryName: string, + include: string, + modules: string, + moduleLookups: $ReadOnlyArray<{ + hasteModuleName: string, + moduleName: string, + }>, +}>) => { + return ` +/** + * 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: GenerateModuleJniCpp.js + */ + +#include ${include} + +namespace facebook::react { + +${modules} + +std::shared_ptr ${libraryName}_ModuleProvider(const std::string &moduleName, const JavaTurboModule::InitParams ¶ms) { +${moduleLookups.map(ModuleLookupTemplate).join('\n')} + return nullptr; +} + +} // namespace facebook::react +`; +}; + +function translateReturnTypeToKind( + nullableTypeAnnotation: Nullable, + resolveAlias: AliasResolver, +): JSReturnType { + const [typeAnnotation] = unwrapNullable( + nullableTypeAnnotation, + ); + let realTypeAnnotation = typeAnnotation; + if (realTypeAnnotation.type === 'TypeAliasTypeAnnotation') { + realTypeAnnotation = resolveAlias(realTypeAnnotation.name); + } + + switch (realTypeAnnotation.type) { + case 'ReservedTypeAnnotation': + switch (realTypeAnnotation.name) { + case 'RootTag': + return 'NumberKind'; + default: + (realTypeAnnotation.name: empty); + throw new Error( + `Invalid ReservedFunctionValueTypeName name, got ${realTypeAnnotation.name}`, + ); + } + case 'VoidTypeAnnotation': + return 'VoidKind'; + case 'StringTypeAnnotation': + return 'StringKind'; + case 'BooleanTypeAnnotation': + return 'BooleanKind'; + case 'EnumDeclaration': + switch (typeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return 'NumberKind'; + case 'StringTypeAnnotation': + return 'StringKind'; + default: + throw new Error( + `Unknown enum prop type for returning value, found: ${realTypeAnnotation.type}"`, + ); + } + case 'UnionTypeAnnotation': + switch (typeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return 'NumberKind'; + case 'ObjectTypeAnnotation': + return 'ObjectKind'; + case 'StringTypeAnnotation': + return 'StringKind'; + default: + throw new Error( + `Unsupported union member returning value, found: ${realTypeAnnotation.memberType}"`, + ); + } + case 'NumberTypeAnnotation': + return 'NumberKind'; + case 'DoubleTypeAnnotation': + return 'NumberKind'; + case 'FloatTypeAnnotation': + return 'NumberKind'; + case 'Int32TypeAnnotation': + return 'NumberKind'; + case 'PromiseTypeAnnotation': + return 'PromiseKind'; + case 'GenericObjectTypeAnnotation': + return 'ObjectKind'; + case 'ObjectTypeAnnotation': + return 'ObjectKind'; + case 'ArrayTypeAnnotation': + return 'ArrayKind'; + default: + (realTypeAnnotation.type: 'MixedTypeAnnotation'); + throw new Error( + `Unknown prop type for returning value, found: ${realTypeAnnotation.type}"`, + ); + } +} + +type Param = NamedShape>; + +function translateParamTypeToJniType( + param: Param, + resolveAlias: AliasResolver, +): string { + const {optional, typeAnnotation: nullableTypeAnnotation} = param; + const [typeAnnotation, nullable] = + unwrapNullable(nullableTypeAnnotation); + const isRequired = !optional && !nullable; + + let realTypeAnnotation = typeAnnotation; + if (realTypeAnnotation.type === 'TypeAliasTypeAnnotation') { + realTypeAnnotation = resolveAlias(realTypeAnnotation.name); + } + + switch (realTypeAnnotation.type) { + case 'ReservedTypeAnnotation': + switch (realTypeAnnotation.name) { + case 'RootTag': + return !isRequired ? 'Ljava/lang/Double;' : 'D'; + default: + (realTypeAnnotation.name: empty); + throw new Error( + `Invalid ReservedFunctionValueTypeName name, got ${realTypeAnnotation.name}`, + ); + } + case 'StringTypeAnnotation': + return 'Ljava/lang/String;'; + case 'BooleanTypeAnnotation': + return !isRequired ? 'Ljava/lang/Boolean;' : 'Z'; + case 'EnumDeclaration': + switch (typeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return !isRequired ? 'Ljava/lang/Double;' : 'D'; + case 'StringTypeAnnotation': + return 'Ljava/lang/String;'; + default: + throw new Error( + `Unknown enum prop type for method arg, found: ${realTypeAnnotation.type}"`, + ); + } + case 'UnionTypeAnnotation': + switch (typeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return !isRequired ? 'Ljava/lang/Double;' : 'D'; + case 'ObjectTypeAnnotation': + return 'Lcom/facebook/react/bridge/ReadableMap;'; + case 'StringTypeAnnotation': + return 'Ljava/lang/String;'; + default: + throw new Error( + `Unsupported union prop value, found: ${realTypeAnnotation.memberType}"`, + ); + } + case 'NumberTypeAnnotation': + return !isRequired ? 'Ljava/lang/Double;' : 'D'; + case 'DoubleTypeAnnotation': + return !isRequired ? 'Ljava/lang/Double;' : 'D'; + case 'FloatTypeAnnotation': + return !isRequired ? 'Ljava/lang/Double;' : 'D'; + case 'Int32TypeAnnotation': + return !isRequired ? 'Ljava/lang/Double;' : 'D'; + case 'GenericObjectTypeAnnotation': + return 'Lcom/facebook/react/bridge/ReadableMap;'; + case 'ObjectTypeAnnotation': + return 'Lcom/facebook/react/bridge/ReadableMap;'; + case 'ArrayTypeAnnotation': + return 'Lcom/facebook/react/bridge/ReadableArray;'; + case 'FunctionTypeAnnotation': + return 'Lcom/facebook/react/bridge/Callback;'; + default: + (realTypeAnnotation.type: 'MixedTypeAnnotation'); + throw new Error( + `Unknown prop type for method arg, found: ${realTypeAnnotation.type}"`, + ); + } +} + +function translateReturnTypeToJniType( + nullableTypeAnnotation: Nullable, + resolveAlias: AliasResolver, +): string { + const [typeAnnotation, nullable] = unwrapNullable(nullableTypeAnnotation); + + let realTypeAnnotation = typeAnnotation; + if (realTypeAnnotation.type === 'TypeAliasTypeAnnotation') { + realTypeAnnotation = resolveAlias(realTypeAnnotation.name); + } + + switch (realTypeAnnotation.type) { + case 'ReservedTypeAnnotation': + switch (realTypeAnnotation.name) { + case 'RootTag': + return nullable ? 'Ljava/lang/Double;' : 'D'; + default: + (realTypeAnnotation.name: empty); + throw new Error( + `Invalid ReservedFunctionValueTypeName name, got ${realTypeAnnotation.name}`, + ); + } + case 'VoidTypeAnnotation': + return 'V'; + case 'StringTypeAnnotation': + return 'Ljava/lang/String;'; + case 'BooleanTypeAnnotation': + return nullable ? 'Ljava/lang/Boolean;' : 'Z'; + case 'EnumDeclaration': + switch (typeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return nullable ? 'Ljava/lang/Double;' : 'D'; + case 'StringTypeAnnotation': + return 'Ljava/lang/String;'; + default: + throw new Error( + `Unknown enum prop type for method return type, found: ${realTypeAnnotation.type}"`, + ); + } + case 'UnionTypeAnnotation': + switch (typeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return nullable ? 'Ljava/lang/Double;' : 'D'; + case 'ObjectTypeAnnotation': + return 'Lcom/facebook/react/bridge/WritableMap;'; + case 'StringTypeAnnotation': + return 'Ljava/lang/String;'; + default: + throw new Error( + `Unsupported union member type, found: ${realTypeAnnotation.memberType}"`, + ); + } + case 'NumberTypeAnnotation': + return nullable ? 'Ljava/lang/Double;' : 'D'; + case 'DoubleTypeAnnotation': + return nullable ? 'Ljava/lang/Double;' : 'D'; + case 'FloatTypeAnnotation': + return nullable ? 'Ljava/lang/Double;' : 'D'; + case 'Int32TypeAnnotation': + return nullable ? 'Ljava/lang/Double;' : 'D'; + case 'PromiseTypeAnnotation': + return 'Lcom/facebook/react/bridge/Promise;'; + case 'GenericObjectTypeAnnotation': + return 'Lcom/facebook/react/bridge/WritableMap;'; + case 'ObjectTypeAnnotation': + return 'Lcom/facebook/react/bridge/WritableMap;'; + case 'ArrayTypeAnnotation': + return 'Lcom/facebook/react/bridge/WritableArray;'; + default: + (realTypeAnnotation.type: 'MixedTypeAnnotation'); + throw new Error( + `Unknown prop type for method return type, found: ${realTypeAnnotation.type}"`, + ); + } +} + +function translateMethodTypeToJniSignature( + property: NativeModulePropertyShape, + resolveAlias: AliasResolver, +): string { + const {name, typeAnnotation} = property; + let [{returnTypeAnnotation, params}] = + unwrapNullable(typeAnnotation); + + params = [...params]; + let processedReturnTypeAnnotation = returnTypeAnnotation; + const isPromiseReturn = returnTypeAnnotation.type === 'PromiseTypeAnnotation'; + if (isPromiseReturn) { + processedReturnTypeAnnotation = { + type: 'VoidTypeAnnotation', + }; + } + + const argsSignatureParts = params.map(t => { + return translateParamTypeToJniType(t, resolveAlias); + }); + if (isPromiseReturn) { + // Additional promise arg for this case. + argsSignatureParts.push( + translateReturnTypeToJniType(returnTypeAnnotation, resolveAlias), + ); + } + const argsSignature = argsSignatureParts.join(''); + const returnSignature = + name === 'getConstants' + ? 'Ljava/util/Map;' + : translateReturnTypeToJniType( + processedReturnTypeAnnotation, + resolveAlias, + ); + + return `(${argsSignature})${returnSignature}`; +} + +function translateMethodForImplementation( + hasteModuleName: string, + property: NativeModulePropertyShape, + resolveAlias: AliasResolver, +): string { + const [propertyTypeAnnotation] = + unwrapNullable(property.typeAnnotation); + const {returnTypeAnnotation} = propertyTypeAnnotation; + + if ( + property.name === 'getConstants' && + returnTypeAnnotation.type === 'ObjectTypeAnnotation' && + returnTypeAnnotation.properties.length === 0 + ) { + return ''; + } + + return HostFunctionTemplate({ + hasteModuleName, + propertyName: property.name, + jniSignature: translateMethodTypeToJniSignature(property, resolveAlias), + jsReturnType: translateReturnTypeToKind(returnTypeAnnotation, resolveAlias), + }); +} + +module.exports = { + generate( + libraryName: string, + schema: SchemaType, + packageName?: string, + assumeNonnull: boolean = false, + headerPrefix?: string, + ): FilesOutput { + const nativeModules = getModules(schema); + + const modules = Object.keys(nativeModules) + .filter(hasteModuleName => { + const module = nativeModules[hasteModuleName]; + return !( + module.excludedPlatforms != null && + module.excludedPlatforms.includes('android') + ); + }) + .sort() + .map(hasteModuleName => { + const { + aliasMap, + spec: {methods}, + } = nativeModules[hasteModuleName]; + const resolveAlias = createAliasResolver(aliasMap); + + const translatedMethods = methods + .map(property => + translateMethodForImplementation( + hasteModuleName, + property, + resolveAlias, + ), + ) + .join('\n\n'); + + return ( + translatedMethods + + '\n\n' + + ModuleClassConstructorTemplate({ + hasteModuleName, + methods: methods + .map(({name: propertyName, typeAnnotation}) => { + const [{returnTypeAnnotation, params}] = + unwrapNullable( + typeAnnotation, + ); + + if ( + propertyName === 'getConstants' && + returnTypeAnnotation.type === 'ObjectTypeAnnotation' && + returnTypeAnnotation.properties && + returnTypeAnnotation.properties.length === 0 + ) { + return null; + } + + return { + propertyName, + argCount: params.length, + }; + }) + .filter(Boolean), + }) + ); + }) + .join('\n'); + + const moduleLookups: $ReadOnlyArray<{ + hasteModuleName: string, + moduleName: string, + }> = Object.keys(nativeModules) + .filter(hasteModuleName => { + const module = nativeModules[hasteModuleName]; + return !( + module.excludedPlatforms != null && + module.excludedPlatforms.includes('android') + ); + }) + .sort((a, b) => { + const nameA = nativeModules[a].moduleName; + const nameB = nativeModules[b].moduleName; + if (nameA < nameB) { + return -1; + } else if (nameA > nameB) { + return 1; + } + return 0; + }) + .map((hasteModuleName: string) => ({ + moduleName: nativeModules[hasteModuleName].moduleName, + hasteModuleName, + })); + + const fileName = `${libraryName}-generated.cpp`; + const replacedTemplate = FileTemplate({ + modules: modules, + libraryName: libraryName.replace(/-/g, '_'), + moduleLookups, + include: `"${libraryName}.h"`, + }); + return new Map([[`jni/${fileName}`, replacedTemplate]]); + }, +}; diff --git a/packages/react-native-codegen/lib/generators/modules/GenerateModuleJniH.js b/packages/react-native-codegen/lib/generators/modules/GenerateModuleJniH.js new file mode 100644 index 00000000000000..cb175cf28854cc --- /dev/null +++ b/packages/react-native-codegen/lib/generators/modules/GenerateModuleJniH.js @@ -0,0 +1,145 @@ +/** + * 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 _require = require('./Utils'), + getModules = _require.getModules; +const ModuleClassDeclarationTemplate = ({hasteModuleName}) => { + return `/** + * JNI C++ class for module '${hasteModuleName}' + */ +class JSI_EXPORT ${hasteModuleName}SpecJSI : public JavaTurboModule { +public: + ${hasteModuleName}SpecJSI(const JavaTurboModule::InitParams ¶ms); +}; +`; +}; +const HeaderFileTemplate = ({modules, libraryName}) => { + return ` +/** + * 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: GenerateModuleJniH.js + */ + +#pragma once + +#include +#include +#include + +namespace facebook::react { + +${modules} + +JSI_EXPORT +std::shared_ptr ${libraryName}_ModuleProvider(const std::string &moduleName, const JavaTurboModule::InitParams ¶ms); + +} // namespace facebook::react +`; +}; + +// Note: this CMakeLists.txt template includes dependencies for both NativeModule and components. +const CMakeListsTemplate = ({libraryName}) => { + return `# 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. + +cmake_minimum_required(VERSION 3.13) +set(CMAKE_VERBOSE_MAKEFILE on) + +file(GLOB react_codegen_SRCS CONFIGURE_DEPENDS *.cpp react/renderer/components/${libraryName}/*.cpp) + +add_library( + react_codegen_${libraryName} + SHARED + \${react_codegen_SRCS} +) + +target_include_directories(react_codegen_${libraryName} PUBLIC . react/renderer/components/${libraryName}) + +target_link_libraries( + react_codegen_${libraryName} + fbjni + folly_runtime + glog + jsi + ${libraryName !== 'rncore' ? 'react_codegen_rncore' : ''} + react_debug + react_nativemodule_core + react_render_componentregistry + react_render_core + react_render_debug + react_render_graphics + react_render_imagemanager + react_render_mapbuffer + react_utils + rrc_image + rrc_view + turbomodulejsijni + yoga +) + +target_compile_options( + react_codegen_${libraryName} + PRIVATE + -DLOG_TAG=\\"ReactNative\\" + -fexceptions + -frtti + -std=c++20 + -Wall +) +`; +}; +module.exports = { + generate( + libraryName, + schema, + packageName, + assumeNonnull = false, + headerPrefix, + ) { + const nativeModules = getModules(schema); + const modules = Object.keys(nativeModules) + .filter(hasteModuleName => { + const module = nativeModules[hasteModuleName]; + return !( + module.excludedPlatforms != null && + module.excludedPlatforms.includes('android') + ); + }) + .sort() + .map(hasteModuleName => + ModuleClassDeclarationTemplate({ + hasteModuleName, + }), + ) + .join('\n'); + const fileName = `${libraryName}.h`; + const replacedTemplate = HeaderFileTemplate({ + modules: modules, + libraryName: libraryName.replace(/-/g, '_'), + }); + return new Map([ + [`jni/${fileName}`, replacedTemplate], + [ + 'jni/CMakeLists.txt', + CMakeListsTemplate({ + libraryName: libraryName, + }), + ], + ]); + }, +}; diff --git a/packages/react-native-codegen/lib/generators/modules/GenerateModuleJniH.js.flow b/packages/react-native-codegen/lib/generators/modules/GenerateModuleJniH.js.flow new file mode 100644 index 00000000000000..0bb2b35d15cbf1 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/modules/GenerateModuleJniH.js.flow @@ -0,0 +1,150 @@ +/** + * 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 {SchemaType} from '../../CodegenSchema'; + +const {getModules} = require('./Utils'); + +type FilesOutput = Map; + +const ModuleClassDeclarationTemplate = ({ + hasteModuleName, +}: $ReadOnly<{hasteModuleName: string}>) => { + return `/** + * JNI C++ class for module '${hasteModuleName}' + */ +class JSI_EXPORT ${hasteModuleName}SpecJSI : public JavaTurboModule { +public: + ${hasteModuleName}SpecJSI(const JavaTurboModule::InitParams ¶ms); +}; +`; +}; + +const HeaderFileTemplate = ({ + modules, + libraryName, +}: $ReadOnly<{modules: string, libraryName: string}>) => { + return ` +/** + * 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: GenerateModuleJniH.js + */ + +#pragma once + +#include +#include +#include + +namespace facebook::react { + +${modules} + +JSI_EXPORT +std::shared_ptr ${libraryName}_ModuleProvider(const std::string &moduleName, const JavaTurboModule::InitParams ¶ms); + +} // namespace facebook::react +`; +}; + +// Note: this CMakeLists.txt template includes dependencies for both NativeModule and components. +const CMakeListsTemplate = ({ + libraryName, +}: $ReadOnly<{libraryName: string}>) => { + return `# 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. + +cmake_minimum_required(VERSION 3.13) +set(CMAKE_VERBOSE_MAKEFILE on) + +file(GLOB react_codegen_SRCS CONFIGURE_DEPENDS *.cpp react/renderer/components/${libraryName}/*.cpp) + +add_library( + react_codegen_${libraryName} + SHARED + \${react_codegen_SRCS} +) + +target_include_directories(react_codegen_${libraryName} PUBLIC . react/renderer/components/${libraryName}) + +target_link_libraries( + react_codegen_${libraryName} + fbjni + folly_runtime + glog + jsi + ${libraryName !== 'rncore' ? 'react_codegen_rncore' : ''} + react_debug + react_nativemodule_core + react_render_componentregistry + react_render_core + react_render_debug + react_render_graphics + react_render_imagemanager + react_render_mapbuffer + react_utils + rrc_image + rrc_view + turbomodulejsijni + yoga +) + +target_compile_options( + react_codegen_${libraryName} + PRIVATE + -DLOG_TAG=\\"ReactNative\\" + -fexceptions + -frtti + -std=c++20 + -Wall +) +`; +}; + +module.exports = { + generate( + libraryName: string, + schema: SchemaType, + packageName?: string, + assumeNonnull: boolean = false, + headerPrefix?: string, + ): FilesOutput { + const nativeModules = getModules(schema); + const modules = Object.keys(nativeModules) + .filter(hasteModuleName => { + const module = nativeModules[hasteModuleName]; + return !( + module.excludedPlatforms != null && + module.excludedPlatforms.includes('android') + ); + }) + .sort() + .map(hasteModuleName => ModuleClassDeclarationTemplate({hasteModuleName})) + .join('\n'); + + const fileName = `${libraryName}.h`; + const replacedTemplate = HeaderFileTemplate({ + modules: modules, + libraryName: libraryName.replace(/-/g, '_'), + }); + return new Map([ + [`jni/${fileName}`, replacedTemplate], + ['jni/CMakeLists.txt', CMakeListsTemplate({libraryName: libraryName})], + ]); + }, +}; diff --git a/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/StructCollector.js b/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/StructCollector.js new file mode 100644 index 00000000000000..17245d3bb8d766 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/StructCollector.js @@ -0,0 +1,258 @@ +/** + * 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'; + +function ownKeys(e, r) { + var t = Object.keys(e); + if (Object.getOwnPropertySymbols) { + var o = Object.getOwnPropertySymbols(e); + r && + (o = o.filter(function (r) { + return Object.getOwnPropertyDescriptor(e, r).enumerable; + })), + t.push.apply(t, o); + } + return t; +} +function _objectSpread(e) { + for (var r = 1; r < arguments.length; r++) { + var t = null != arguments[r] ? arguments[r] : {}; + r % 2 + ? ownKeys(Object(t), !0).forEach(function (r) { + _defineProperty(e, r, t[r]); + }) + : Object.getOwnPropertyDescriptors + ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) + : ownKeys(Object(t)).forEach(function (r) { + Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); + }); + } + return e; +} +function _slicedToArray(r, e) { + return ( + _arrayWithHoles(r) || + _iterableToArrayLimit(r, e) || + _unsupportedIterableToArray(r, e) || + _nonIterableRest() + ); +} +function _nonIterableRest() { + throw new TypeError( + 'Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.', + ); +} +function _unsupportedIterableToArray(r, a) { + if (r) { + if ('string' == typeof r) return _arrayLikeToArray(r, a); + var t = {}.toString.call(r).slice(8, -1); + return ( + 'Object' === t && r.constructor && (t = r.constructor.name), + 'Map' === t || 'Set' === t + ? Array.from(r) + : 'Arguments' === t || + /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) + ? _arrayLikeToArray(r, a) + : void 0 + ); + } +} +function _arrayLikeToArray(r, a) { + (null == a || a > r.length) && (a = r.length); + for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; + return n; +} +function _iterableToArrayLimit(r, l) { + var t = + null == r + ? null + : ('undefined' != typeof Symbol && r[Symbol.iterator]) || r['@@iterator']; + if (null != t) { + var e, + n, + i, + u, + a = [], + f = !0, + o = !1; + try { + if (((i = (t = t.call(r)).next), 0 === l)) { + if (Object(t) !== t) return; + f = !1; + } else + for ( + ; + !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); + f = !0 + ); + } catch (r) { + (o = !0), (n = r); + } finally { + try { + if (!f && null != t.return && ((u = t.return()), Object(u) !== u)) + return; + } finally { + if (o) throw n; + } + } + return a; + } +} +function _arrayWithHoles(r) { + if (Array.isArray(r)) return r; +} +function _defineProperty(e, r, t) { + return ( + (r = _toPropertyKey(r)) in e + ? Object.defineProperty(e, r, { + value: t, + enumerable: !0, + configurable: !0, + writable: !0, + }) + : (e[r] = t), + e + ); +} +function _toPropertyKey(t) { + var i = _toPrimitive(t, 'string'); + return 'symbol' == typeof i ? i : i + ''; +} +function _toPrimitive(t, r) { + if ('object' != typeof t || !t) return t; + var e = t[Symbol.toPrimitive]; + if (void 0 !== e) { + var i = e.call(t, r || 'default'); + if ('object' != typeof i) return i; + throw new TypeError('@@toPrimitive must return a primitive value.'); + } + return ('string' === r ? String : Number)(t); +} +const _require = require('../../../parsers/parsers-commons'), + unwrapNullable = _require.unwrapNullable, + wrapNullable = _require.wrapNullable; +const _require2 = require('../../Utils'), + capitalize = _require2.capitalize; +class StructCollector { + constructor() { + _defineProperty(this, '_structs', new Map()); + } + process(structName, structContext, resolveAlias, nullableTypeAnnotation) { + const _unwrapNullable = unwrapNullable(nullableTypeAnnotation), + _unwrapNullable2 = _slicedToArray(_unwrapNullable, 2), + typeAnnotation = _unwrapNullable2[0], + nullable = _unwrapNullable2[1]; + switch (typeAnnotation.type) { + case 'ObjectTypeAnnotation': { + this._insertStruct( + structName, + structContext, + resolveAlias, + typeAnnotation, + ); + return wrapNullable(nullable, { + type: 'TypeAliasTypeAnnotation', + name: structName, + }); + } + case 'ArrayTypeAnnotation': { + if (typeAnnotation.elementType == null) { + return wrapNullable(nullable, { + type: 'ArrayTypeAnnotation', + }); + } + return wrapNullable(nullable, { + type: 'ArrayTypeAnnotation', + elementType: this.process( + structName + 'Element', + structContext, + resolveAlias, + typeAnnotation.elementType, + ), + }); + } + case 'TypeAliasTypeAnnotation': { + this._insertAlias(typeAnnotation.name, structContext, resolveAlias); + return wrapNullable(nullable, typeAnnotation); + } + case 'EnumDeclaration': + return wrapNullable(nullable, typeAnnotation); + case 'MixedTypeAnnotation': + throw new Error('Mixed types are unsupported in structs'); + case 'UnionTypeAnnotation': + throw new Error('Union types are unsupported in structs'); + default: { + return wrapNullable(nullable, typeAnnotation); + } + } + } + _insertAlias(aliasName, structContext, resolveAlias) { + const usedStruct = this._structs.get(aliasName); + if (usedStruct == null) { + this._insertStruct( + aliasName, + structContext, + resolveAlias, + resolveAlias(aliasName), + ); + } else if (usedStruct.context !== structContext) { + throw new Error( + `Tried to use alias '${aliasName}' in a getConstants() return type and inside a regular struct.`, + ); + } + } + _insertStruct(structName, structContext, resolveAlias, objectTypeAnnotation) { + // $FlowFixMe[missing-type-arg] + const properties = objectTypeAnnotation.properties.map(property => { + const propertyStructName = structName + capitalize(property.name); + return _objectSpread( + _objectSpread({}, property), + {}, + { + typeAnnotation: this.process( + propertyStructName, + structContext, + resolveAlias, + property.typeAnnotation, + ), + }, + ); + }); + switch (structContext) { + case 'REGULAR': + this._structs.set(structName, { + name: structName, + context: 'REGULAR', + properties: properties, + }); + break; + case 'CONSTANTS': + this._structs.set(structName, { + name: structName, + context: 'CONSTANTS', + properties: properties, + }); + break; + default: + structContext; + throw new Error(`Detected an invalid struct context: ${structContext}`); + } + } + getAllStructs() { + return [...this._structs.values()]; + } + getStruct(name) { + return this._structs.get(name); + } +} +module.exports = { + StructCollector, +}; diff --git a/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/StructCollector.js.flow b/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/StructCollector.js.flow new file mode 100644 index 00000000000000..d85d093948ad27 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/StructCollector.js.flow @@ -0,0 +1,207 @@ +/** + * 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 { + NativeModuleArrayTypeAnnotation, + NativeModuleBaseTypeAnnotation, + NativeModuleBooleanTypeAnnotation, + NativeModuleDoubleTypeAnnotation, + NativeModuleEnumDeclaration, + NativeModuleFloatTypeAnnotation, + NativeModuleGenericObjectTypeAnnotation, + NativeModuleInt32TypeAnnotation, + NativeModuleNumberTypeAnnotation, + NativeModuleObjectTypeAnnotation, + NativeModuleStringTypeAnnotation, + NativeModuleTypeAliasTypeAnnotation, + Nullable, + ReservedTypeAnnotation, +} from '../../../CodegenSchema'; +import type {AliasResolver} from '../Utils'; + +const { + unwrapNullable, + wrapNullable, +} = require('../../../parsers/parsers-commons'); +const {capitalize} = require('../../Utils'); + +type StructContext = 'CONSTANTS' | 'REGULAR'; + +export type RegularStruct = $ReadOnly<{ + context: 'REGULAR', + name: string, + properties: $ReadOnlyArray, +}>; + +export type ConstantsStruct = $ReadOnly<{ + context: 'CONSTANTS', + name: string, + properties: $ReadOnlyArray, +}>; + +export type Struct = RegularStruct | ConstantsStruct; + +export type StructProperty = $ReadOnly<{ + name: string, + optional: boolean, + typeAnnotation: Nullable, +}>; + +export type StructTypeAnnotation = + | NativeModuleStringTypeAnnotation + | NativeModuleNumberTypeAnnotation + | NativeModuleInt32TypeAnnotation + | NativeModuleDoubleTypeAnnotation + | NativeModuleFloatTypeAnnotation + | NativeModuleBooleanTypeAnnotation + | NativeModuleEnumDeclaration + | NativeModuleGenericObjectTypeAnnotation + | ReservedTypeAnnotation + | NativeModuleTypeAliasTypeAnnotation + | NativeModuleArrayTypeAnnotation>; + +class StructCollector { + _structs: Map = new Map(); + + process( + structName: string, + structContext: StructContext, + resolveAlias: AliasResolver, + nullableTypeAnnotation: Nullable, + ): Nullable { + const [typeAnnotation, nullable] = unwrapNullable(nullableTypeAnnotation); + switch (typeAnnotation.type) { + case 'ObjectTypeAnnotation': { + this._insertStruct( + structName, + structContext, + resolveAlias, + typeAnnotation, + ); + return wrapNullable(nullable, { + type: 'TypeAliasTypeAnnotation', + name: structName, + }); + } + case 'ArrayTypeAnnotation': { + if (typeAnnotation.elementType == null) { + return wrapNullable(nullable, { + type: 'ArrayTypeAnnotation', + }); + } + + return wrapNullable(nullable, { + type: 'ArrayTypeAnnotation', + elementType: this.process( + structName + 'Element', + structContext, + resolveAlias, + typeAnnotation.elementType, + ), + }); + } + case 'TypeAliasTypeAnnotation': { + this._insertAlias(typeAnnotation.name, structContext, resolveAlias); + return wrapNullable(nullable, typeAnnotation); + } + case 'EnumDeclaration': + return wrapNullable(nullable, typeAnnotation); + case 'MixedTypeAnnotation': + throw new Error('Mixed types are unsupported in structs'); + case 'UnionTypeAnnotation': + throw new Error('Union types are unsupported in structs'); + default: { + return wrapNullable(nullable, typeAnnotation); + } + } + } + + _insertAlias( + aliasName: string, + structContext: StructContext, + resolveAlias: AliasResolver, + ): void { + const usedStruct = this._structs.get(aliasName); + if (usedStruct == null) { + this._insertStruct( + aliasName, + structContext, + resolveAlias, + resolveAlias(aliasName), + ); + } else if (usedStruct.context !== structContext) { + throw new Error( + `Tried to use alias '${aliasName}' in a getConstants() return type and inside a regular struct.`, + ); + } + } + + _insertStruct( + structName: string, + structContext: StructContext, + resolveAlias: AliasResolver, + objectTypeAnnotation: NativeModuleObjectTypeAnnotation, + ): void { + // $FlowFixMe[missing-type-arg] + const properties = objectTypeAnnotation.properties.map< + $ReadOnly<{ + name: string, + optional: boolean, + typeAnnotation: Nullable, + }>, + >(property => { + const propertyStructName = structName + capitalize(property.name); + + return { + ...property, + typeAnnotation: this.process( + propertyStructName, + structContext, + resolveAlias, + property.typeAnnotation, + ), + }; + }); + + switch (structContext) { + case 'REGULAR': + this._structs.set(structName, { + name: structName, + context: 'REGULAR', + properties: properties, + }); + break; + case 'CONSTANTS': + this._structs.set(structName, { + name: structName, + context: 'CONSTANTS', + properties: properties, + }); + break; + default: + (structContext: empty); + throw new Error(`Detected an invalid struct context: ${structContext}`); + } + } + + getAllStructs(): $ReadOnlyArray { + return [...this._structs.values()]; + } + + getStruct(name: string): ?Struct { + return this._structs.get(name); + } +} + +module.exports = { + StructCollector, +}; diff --git a/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/Utils.js b/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/Utils.js new file mode 100644 index 00000000000000..b4be0c66d3fc50 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/Utils.js @@ -0,0 +1,25 @@ +/** + * 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'; + +function getSafePropertyName(property) { + if (property.name === 'id') { + return `${property.name}_`; + } + return property.name; +} +function getNamespacedStructName(hasteModuleName, structName) { + return `JS::${hasteModuleName}::${structName}`; +} +module.exports = { + getSafePropertyName, + getNamespacedStructName, +}; diff --git a/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/Utils.js.flow b/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/Utils.js.flow new file mode 100644 index 00000000000000..59121a4b52faa1 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/Utils.js.flow @@ -0,0 +1,32 @@ +/** + * 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 {StructProperty} from './StructCollector'; + +function getSafePropertyName(property: StructProperty): string { + if (property.name === 'id') { + return `${property.name}_`; + } + return property.name; +} + +function getNamespacedStructName( + hasteModuleName: string, + structName: string, +): string { + return `JS::${hasteModuleName}::${structName}`; +} + +module.exports = { + getSafePropertyName, + getNamespacedStructName, +}; diff --git a/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/header/serializeConstantsStruct.js b/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/header/serializeConstantsStruct.js new file mode 100644 index 00000000000000..64b6a8a95f8f47 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/header/serializeConstantsStruct.js @@ -0,0 +1,343 @@ +/** + * 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'; + +function _slicedToArray(r, e) { + return ( + _arrayWithHoles(r) || + _iterableToArrayLimit(r, e) || + _unsupportedIterableToArray(r, e) || + _nonIterableRest() + ); +} +function _nonIterableRest() { + throw new TypeError( + 'Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.', + ); +} +function _unsupportedIterableToArray(r, a) { + if (r) { + if ('string' == typeof r) return _arrayLikeToArray(r, a); + var t = {}.toString.call(r).slice(8, -1); + return ( + 'Object' === t && r.constructor && (t = r.constructor.name), + 'Map' === t || 'Set' === t + ? Array.from(r) + : 'Arguments' === t || + /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) + ? _arrayLikeToArray(r, a) + : void 0 + ); + } +} +function _arrayLikeToArray(r, a) { + (null == a || a > r.length) && (a = r.length); + for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; + return n; +} +function _iterableToArrayLimit(r, l) { + var t = + null == r + ? null + : ('undefined' != typeof Symbol && r[Symbol.iterator]) || r['@@iterator']; + if (null != t) { + var e, + n, + i, + u, + a = [], + f = !0, + o = !1; + try { + if (((i = (t = t.call(r)).next), 0 === l)) { + if (Object(t) !== t) return; + f = !1; + } else + for ( + ; + !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); + f = !0 + ); + } catch (r) { + (o = !0), (n = r); + } finally { + try { + if (!f && null != t.return && ((u = t.return()), Object(u) !== u)) + return; + } finally { + if (o) throw n; + } + } + return a; + } +} +function _arrayWithHoles(r) { + if (Array.isArray(r)) return r; +} +const _require = require('../../../../parsers/parsers-commons'), + unwrapNullable = _require.unwrapNullable; +const _require2 = require('../../../TypeUtils/Cxx'), + wrapCxxOptional = _require2.wrapOptional; +const _require3 = require('../../../TypeUtils/Objective-C'), + wrapObjCOptional = _require3.wrapOptional; +const _require4 = require('../../../Utils'), + capitalize = _require4.capitalize; +const _require5 = require('../Utils'), + getNamespacedStructName = _require5.getNamespacedStructName, + getSafePropertyName = _require5.getSafePropertyName; +const StructTemplate = ({ + hasteModuleName, + structName, + builderInputProps, +}) => `namespace JS { + namespace ${hasteModuleName} { + struct ${structName} { + + struct Builder { + struct Input { + ${builderInputProps} + }; + + /** Initialize with a set of values */ + Builder(const Input i); + /** Initialize with an existing ${structName} */ + Builder(${structName} i); + /** Builds the object. Generally used only by the infrastructure. */ + NSDictionary *buildUnsafeRawValue() const { return _factory(); }; + private: + NSDictionary *(^_factory)(void); + }; + + static ${structName} fromUnsafeRawValue(NSDictionary *const v) { return {v}; } + NSDictionary *unsafeRawValue() const { return _v; } + private: + ${structName}(NSDictionary *const v) : _v(v) {} + NSDictionary *_v; + }; + } +}`; +const MethodTemplate = ({ + hasteModuleName, + structName, + properties, +}) => `inline JS::${hasteModuleName}::${structName}::Builder::Builder(const Input i) : _factory(^{ + NSMutableDictionary *d = [NSMutableDictionary new]; +${properties} + return d; +}) {} +inline JS::${hasteModuleName}::${structName}::Builder::Builder(${structName} i) : _factory(^{ + return i.unsafeRawValue(); +}) {}`; +function toObjCType( + hasteModuleName, + nullableTypeAnnotation, + isOptional = false, +) { + const _unwrapNullable = unwrapNullable(nullableTypeAnnotation), + _unwrapNullable2 = _slicedToArray(_unwrapNullable, 2), + typeAnnotation = _unwrapNullable2[0], + nullable = _unwrapNullable2[1]; + const isRequired = !nullable && !isOptional; + switch (typeAnnotation.type) { + case 'ReservedTypeAnnotation': + switch (typeAnnotation.name) { + case 'RootTag': + return wrapCxxOptional('double', isRequired); + default: + typeAnnotation.name; + throw new Error(`Unknown prop type, found: ${typeAnnotation.name}"`); + } + case 'StringTypeAnnotation': + return 'NSString *'; + case 'NumberTypeAnnotation': + return wrapCxxOptional('double', isRequired); + case 'FloatTypeAnnotation': + return wrapCxxOptional('double', isRequired); + case 'Int32TypeAnnotation': + return wrapCxxOptional('double', isRequired); + case 'DoubleTypeAnnotation': + return wrapCxxOptional('double', isRequired); + case 'BooleanTypeAnnotation': + return wrapCxxOptional('bool', isRequired); + case 'EnumDeclaration': + switch (typeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return wrapCxxOptional('double', isRequired); + case 'StringTypeAnnotation': + return 'NSString *'; + default: + throw new Error( + `Couldn't convert enum into ObjC type: ${typeAnnotation.type}"`, + ); + } + case 'GenericObjectTypeAnnotation': + return wrapObjCOptional('id', isRequired); + case 'ArrayTypeAnnotation': + if (typeAnnotation.elementType == null) { + return wrapObjCOptional('id', isRequired); + } + return wrapCxxOptional( + `std::vector<${toObjCType( + hasteModuleName, + typeAnnotation.elementType, + )}>`, + isRequired, + ); + case 'TypeAliasTypeAnnotation': + const structName = capitalize(typeAnnotation.name); + const namespacedStructName = getNamespacedStructName( + hasteModuleName, + structName, + ); + return wrapCxxOptional(`${namespacedStructName}::Builder`, isRequired); + default: + typeAnnotation.type; + throw new Error( + `Couldn't convert into ObjC type: ${typeAnnotation.type}"`, + ); + } +} +function toObjCValue( + hasteModuleName, + nullableTypeAnnotation, + value, + depth, + isOptional = false, +) { + const _unwrapNullable3 = unwrapNullable(nullableTypeAnnotation), + _unwrapNullable4 = _slicedToArray(_unwrapNullable3, 2), + typeAnnotation = _unwrapNullable4[0], + nullable = _unwrapNullable4[1]; + const isRequired = !nullable && !isOptional; + function wrapPrimitive(type) { + return !isRequired + ? `${value}.has_value() ? @((${type})${value}.value()) : nil` + : `@(${value})`; + } + switch (typeAnnotation.type) { + case 'ReservedTypeAnnotation': + switch (typeAnnotation.name) { + case 'RootTag': + return wrapPrimitive('double'); + default: + typeAnnotation.name; + throw new Error( + `Couldn't convert into ObjC type: ${typeAnnotation.type}"`, + ); + } + case 'StringTypeAnnotation': + return value; + case 'NumberTypeAnnotation': + return wrapPrimitive('double'); + case 'FloatTypeAnnotation': + return wrapPrimitive('double'); + case 'Int32TypeAnnotation': + return wrapPrimitive('double'); + case 'DoubleTypeAnnotation': + return wrapPrimitive('double'); + case 'BooleanTypeAnnotation': + return wrapPrimitive('BOOL'); + case 'EnumDeclaration': + switch (typeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return wrapPrimitive('double'); + case 'StringTypeAnnotation': + return value; + default: + throw new Error( + `Couldn't convert enum into ObjC value: ${typeAnnotation.type}"`, + ); + } + case 'GenericObjectTypeAnnotation': + return value; + case 'ArrayTypeAnnotation': + const elementType = typeAnnotation.elementType; + if (elementType == null) { + return value; + } + const localVarName = `el${'_'.repeat(depth + 1)}`; + const elementObjCType = toObjCType(hasteModuleName, elementType); + const elementObjCValue = toObjCValue( + hasteModuleName, + elementType, + localVarName, + depth + 1, + ); + const RCTConvertVecToArray = transformer => { + return `RCTConvert${ + !isRequired ? 'Optional' : '' + }VecToArray(${value}, ${transformer})`; + }; + return RCTConvertVecToArray( + `^id(${elementObjCType} ${localVarName}) { return ${elementObjCValue}; }`, + ); + case 'TypeAliasTypeAnnotation': + return !isRequired + ? `${value}.has_value() ? ${value}.value().buildUnsafeRawValue() : nil` + : `${value}.buildUnsafeRawValue()`; + default: + typeAnnotation.type; + throw new Error( + `Couldn't convert into ObjC value: ${typeAnnotation.type}"`, + ); + } +} +function serializeConstantsStruct(hasteModuleName, struct) { + const declaration = StructTemplate({ + hasteModuleName, + structName: struct.name, + builderInputProps: struct.properties + .map(property => { + const typeAnnotation = property.typeAnnotation, + optional = property.optional; + const safePropName = getSafePropertyName(property); + const objCType = toObjCType(hasteModuleName, typeAnnotation, optional); + if (!optional) { + return `RCTRequired<${objCType}> ${safePropName};`; + } + const space = ' '.repeat(objCType.endsWith('*') ? 0 : 1); + return `${objCType}${space}${safePropName};`; + }) + .join('\n '), + }); + const methods = MethodTemplate({ + hasteModuleName, + structName: struct.name, + properties: struct.properties + .map(property => { + const typeAnnotation = property.typeAnnotation, + optional = property.optional, + propName = property.name; + const safePropName = getSafePropertyName(property); + const objCValue = toObjCValue( + hasteModuleName, + typeAnnotation, + safePropName, + 0, + optional, + ); + let varDecl = `auto ${safePropName} = i.${safePropName}`; + if (!optional) { + varDecl += '.get()'; + } + const assignment = `d[@"${propName}"] = ` + objCValue; + return ` ${varDecl};\n ${assignment};`; + }) + .join('\n'), + }); + return { + declaration, + methods, + }; +} +module.exports = { + serializeConstantsStruct, +}; diff --git a/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/header/serializeConstantsStruct.js.flow b/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/header/serializeConstantsStruct.js.flow new file mode 100644 index 00000000000000..ad7918d686f855 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/header/serializeConstantsStruct.js.flow @@ -0,0 +1,289 @@ +/** + * 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 {Nullable} from '../../../../CodegenSchema'; +import type {ConstantsStruct, StructTypeAnnotation} from '../StructCollector'; +import type {StructSerilizationOutput} from './serializeStruct'; + +const {unwrapNullable} = require('../../../../parsers/parsers-commons'); +const {wrapOptional: wrapCxxOptional} = require('../../../TypeUtils/Cxx'); +const { + wrapOptional: wrapObjCOptional, +} = require('../../../TypeUtils/Objective-C'); +const {capitalize} = require('../../../Utils'); +const {getNamespacedStructName, getSafePropertyName} = require('../Utils'); + +const StructTemplate = ({ + hasteModuleName, + structName, + builderInputProps, +}: $ReadOnly<{ + hasteModuleName: string, + structName: string, + builderInputProps: string, +}>) => `namespace JS { + namespace ${hasteModuleName} { + struct ${structName} { + + struct Builder { + struct Input { + ${builderInputProps} + }; + + /** Initialize with a set of values */ + Builder(const Input i); + /** Initialize with an existing ${structName} */ + Builder(${structName} i); + /** Builds the object. Generally used only by the infrastructure. */ + NSDictionary *buildUnsafeRawValue() const { return _factory(); }; + private: + NSDictionary *(^_factory)(void); + }; + + static ${structName} fromUnsafeRawValue(NSDictionary *const v) { return {v}; } + NSDictionary *unsafeRawValue() const { return _v; } + private: + ${structName}(NSDictionary *const v) : _v(v) {} + NSDictionary *_v; + }; + } +}`; + +const MethodTemplate = ({ + hasteModuleName, + structName, + properties, +}: $ReadOnly<{ + hasteModuleName: string, + structName: string, + properties: string, +}>) => `inline JS::${hasteModuleName}::${structName}::Builder::Builder(const Input i) : _factory(^{ + NSMutableDictionary *d = [NSMutableDictionary new]; +${properties} + return d; +}) {} +inline JS::${hasteModuleName}::${structName}::Builder::Builder(${structName} i) : _factory(^{ + return i.unsafeRawValue(); +}) {}`; + +function toObjCType( + hasteModuleName: string, + nullableTypeAnnotation: Nullable, + isOptional: boolean = false, +): string { + const [typeAnnotation, nullable] = unwrapNullable(nullableTypeAnnotation); + const isRequired = !nullable && !isOptional; + + switch (typeAnnotation.type) { + case 'ReservedTypeAnnotation': + switch (typeAnnotation.name) { + case 'RootTag': + return wrapCxxOptional('double', isRequired); + default: + (typeAnnotation.name: empty); + throw new Error(`Unknown prop type, found: ${typeAnnotation.name}"`); + } + case 'StringTypeAnnotation': + return 'NSString *'; + case 'NumberTypeAnnotation': + return wrapCxxOptional('double', isRequired); + case 'FloatTypeAnnotation': + return wrapCxxOptional('double', isRequired); + case 'Int32TypeAnnotation': + return wrapCxxOptional('double', isRequired); + case 'DoubleTypeAnnotation': + return wrapCxxOptional('double', isRequired); + case 'BooleanTypeAnnotation': + return wrapCxxOptional('bool', isRequired); + case 'EnumDeclaration': + switch (typeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return wrapCxxOptional('double', isRequired); + case 'StringTypeAnnotation': + return 'NSString *'; + default: + throw new Error( + `Couldn't convert enum into ObjC type: ${typeAnnotation.type}"`, + ); + } + case 'GenericObjectTypeAnnotation': + return wrapObjCOptional('id', isRequired); + case 'ArrayTypeAnnotation': + if (typeAnnotation.elementType == null) { + return wrapObjCOptional('id', isRequired); + } + + return wrapCxxOptional( + `std::vector<${toObjCType( + hasteModuleName, + typeAnnotation.elementType, + )}>`, + isRequired, + ); + case 'TypeAliasTypeAnnotation': + const structName = capitalize(typeAnnotation.name); + const namespacedStructName = getNamespacedStructName( + hasteModuleName, + structName, + ); + return wrapCxxOptional(`${namespacedStructName}::Builder`, isRequired); + default: + (typeAnnotation.type: empty); + throw new Error( + `Couldn't convert into ObjC type: ${typeAnnotation.type}"`, + ); + } +} + +function toObjCValue( + hasteModuleName: string, + nullableTypeAnnotation: Nullable, + value: string, + depth: number, + isOptional: boolean = false, +): string { + const [typeAnnotation, nullable] = unwrapNullable(nullableTypeAnnotation); + const isRequired = !nullable && !isOptional; + + function wrapPrimitive(type: string) { + return !isRequired + ? `${value}.has_value() ? @((${type})${value}.value()) : nil` + : `@(${value})`; + } + + switch (typeAnnotation.type) { + case 'ReservedTypeAnnotation': + switch (typeAnnotation.name) { + case 'RootTag': + return wrapPrimitive('double'); + default: + (typeAnnotation.name: empty); + throw new Error( + `Couldn't convert into ObjC type: ${typeAnnotation.type}"`, + ); + } + case 'StringTypeAnnotation': + return value; + case 'NumberTypeAnnotation': + return wrapPrimitive('double'); + case 'FloatTypeAnnotation': + return wrapPrimitive('double'); + case 'Int32TypeAnnotation': + return wrapPrimitive('double'); + case 'DoubleTypeAnnotation': + return wrapPrimitive('double'); + case 'BooleanTypeAnnotation': + return wrapPrimitive('BOOL'); + case 'EnumDeclaration': + switch (typeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return wrapPrimitive('double'); + case 'StringTypeAnnotation': + return value; + default: + throw new Error( + `Couldn't convert enum into ObjC value: ${typeAnnotation.type}"`, + ); + } + case 'GenericObjectTypeAnnotation': + return value; + case 'ArrayTypeAnnotation': + const {elementType} = typeAnnotation; + if (elementType == null) { + return value; + } + + const localVarName = `el${'_'.repeat(depth + 1)}`; + const elementObjCType = toObjCType(hasteModuleName, elementType); + const elementObjCValue = toObjCValue( + hasteModuleName, + elementType, + localVarName, + depth + 1, + ); + + const RCTConvertVecToArray = (transformer: string) => { + return `RCTConvert${ + !isRequired ? 'Optional' : '' + }VecToArray(${value}, ${transformer})`; + }; + + return RCTConvertVecToArray( + `^id(${elementObjCType} ${localVarName}) { return ${elementObjCValue}; }`, + ); + case 'TypeAliasTypeAnnotation': + return !isRequired + ? `${value}.has_value() ? ${value}.value().buildUnsafeRawValue() : nil` + : `${value}.buildUnsafeRawValue()`; + default: + (typeAnnotation.type: empty); + throw new Error( + `Couldn't convert into ObjC value: ${typeAnnotation.type}"`, + ); + } +} + +function serializeConstantsStruct( + hasteModuleName: string, + struct: ConstantsStruct, +): StructSerilizationOutput { + const declaration = StructTemplate({ + hasteModuleName, + structName: struct.name, + builderInputProps: struct.properties + .map(property => { + const {typeAnnotation, optional} = property; + const safePropName = getSafePropertyName(property); + const objCType = toObjCType(hasteModuleName, typeAnnotation, optional); + + if (!optional) { + return `RCTRequired<${objCType}> ${safePropName};`; + } + + const space = ' '.repeat(objCType.endsWith('*') ? 0 : 1); + return `${objCType}${space}${safePropName};`; + }) + .join('\n '), + }); + + const methods = MethodTemplate({ + hasteModuleName, + structName: struct.name, + properties: struct.properties + .map(property => { + const {typeAnnotation, optional, name: propName} = property; + const safePropName = getSafePropertyName(property); + const objCValue = toObjCValue( + hasteModuleName, + typeAnnotation, + safePropName, + 0, + optional, + ); + + let varDecl = `auto ${safePropName} = i.${safePropName}`; + if (!optional) { + varDecl += '.get()'; + } + + const assignment = `d[@"${propName}"] = ` + objCValue; + return ` ${varDecl};\n ${assignment};`; + }) + .join('\n'), + }); + + return {declaration, methods}; +} + +module.exports = { + serializeConstantsStruct, +}; diff --git a/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/header/serializeRegularStruct.js b/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/header/serializeRegularStruct.js new file mode 100644 index 00000000000000..d58eb563281f21 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/header/serializeRegularStruct.js @@ -0,0 +1,336 @@ +/** + * 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'; + +function _slicedToArray(r, e) { + return ( + _arrayWithHoles(r) || + _iterableToArrayLimit(r, e) || + _unsupportedIterableToArray(r, e) || + _nonIterableRest() + ); +} +function _nonIterableRest() { + throw new TypeError( + 'Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.', + ); +} +function _unsupportedIterableToArray(r, a) { + if (r) { + if ('string' == typeof r) return _arrayLikeToArray(r, a); + var t = {}.toString.call(r).slice(8, -1); + return ( + 'Object' === t && r.constructor && (t = r.constructor.name), + 'Map' === t || 'Set' === t + ? Array.from(r) + : 'Arguments' === t || + /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) + ? _arrayLikeToArray(r, a) + : void 0 + ); + } +} +function _arrayLikeToArray(r, a) { + (null == a || a > r.length) && (a = r.length); + for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; + return n; +} +function _iterableToArrayLimit(r, l) { + var t = + null == r + ? null + : ('undefined' != typeof Symbol && r[Symbol.iterator]) || r['@@iterator']; + if (null != t) { + var e, + n, + i, + u, + a = [], + f = !0, + o = !1; + try { + if (((i = (t = t.call(r)).next), 0 === l)) { + if (Object(t) !== t) return; + f = !1; + } else + for ( + ; + !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); + f = !0 + ); + } catch (r) { + (o = !0), (n = r); + } finally { + try { + if (!f && null != t.return && ((u = t.return()), Object(u) !== u)) + return; + } finally { + if (o) throw n; + } + } + return a; + } +} +function _arrayWithHoles(r) { + if (Array.isArray(r)) return r; +} +const _require = require('../../../../parsers/parsers-commons'), + unwrapNullable = _require.unwrapNullable; +const _require2 = require('../../../TypeUtils/Cxx'), + wrapCxxOptional = _require2.wrapOptional; +const _require3 = require('../../../TypeUtils/Objective-C'), + wrapObjCOptional = _require3.wrapOptional; +const _require4 = require('../../../Utils'), + capitalize = _require4.capitalize; +const _require5 = require('../Utils'), + getNamespacedStructName = _require5.getNamespacedStructName, + getSafePropertyName = _require5.getSafePropertyName; +const StructTemplate = ({ + hasteModuleName, + structName, + structProperties, +}) => `namespace JS { + namespace ${hasteModuleName} { + struct ${structName} { + ${structProperties} + + ${structName}(NSDictionary *const v) : _v(v) {} + private: + NSDictionary *_v; + }; + } +} + +@interface RCTCxxConvert (${hasteModuleName}_${structName}) ++ (RCTManagedPointer *)JS_${hasteModuleName}_${structName}:(id)json; +@end`; +const MethodTemplate = ({ + returnType, + returnValue, + hasteModuleName, + structName, + propertyName, + safePropertyName, +}) => `inline ${returnType}JS::${hasteModuleName}::${structName}::${safePropertyName}() const +{ + id const p = _v[@"${propertyName}"]; + return ${returnValue}; +}`; +function toObjCType( + hasteModuleName, + nullableTypeAnnotation, + isOptional = false, +) { + const _unwrapNullable = unwrapNullable(nullableTypeAnnotation), + _unwrapNullable2 = _slicedToArray(_unwrapNullable, 2), + typeAnnotation = _unwrapNullable2[0], + nullable = _unwrapNullable2[1]; + const isRequired = !nullable && !isOptional; + switch (typeAnnotation.type) { + case 'ReservedTypeAnnotation': + switch (typeAnnotation.name) { + case 'RootTag': + return wrapCxxOptional('double', isRequired); + default: + typeAnnotation.name; + throw new Error(`Unknown prop type, found: ${typeAnnotation.name}"`); + } + case 'StringTypeAnnotation': + return 'NSString *'; + case 'NumberTypeAnnotation': + return wrapCxxOptional('double', isRequired); + case 'FloatTypeAnnotation': + return wrapCxxOptional('double', isRequired); + case 'Int32TypeAnnotation': + return wrapCxxOptional('double', isRequired); + case 'DoubleTypeAnnotation': + return wrapCxxOptional('double', isRequired); + case 'BooleanTypeAnnotation': + return wrapCxxOptional('bool', isRequired); + case 'EnumDeclaration': + switch (typeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return wrapCxxOptional('double', isRequired); + case 'StringTypeAnnotation': + return 'NSString *'; + default: + throw new Error( + `Couldn't convert enum into ObjC type: ${typeAnnotation.type}"`, + ); + } + case 'GenericObjectTypeAnnotation': + return wrapObjCOptional('id', isRequired); + case 'ArrayTypeAnnotation': + if (typeAnnotation.elementType == null) { + return wrapObjCOptional('id', isRequired); + } + return wrapCxxOptional( + `facebook::react::LazyVector<${toObjCType( + hasteModuleName, + typeAnnotation.elementType, + )}>`, + isRequired, + ); + case 'TypeAliasTypeAnnotation': + const structName = capitalize(typeAnnotation.name); + const namespacedStructName = getNamespacedStructName( + hasteModuleName, + structName, + ); + return wrapCxxOptional(namespacedStructName, isRequired); + default: + typeAnnotation.type; + throw new Error( + `Couldn't convert into ObjC type: ${typeAnnotation.type}"`, + ); + } +} +function toObjCValue( + hasteModuleName, + nullableTypeAnnotation, + value, + depth, + isOptional = false, +) { + const _unwrapNullable3 = unwrapNullable(nullableTypeAnnotation), + _unwrapNullable4 = _slicedToArray(_unwrapNullable3, 2), + typeAnnotation = _unwrapNullable4[0], + nullable = _unwrapNullable4[1]; + const isRequired = !nullable && !isOptional; + const RCTBridgingTo = (type, arg) => { + const args = [value, arg].filter(Boolean).join(', '); + return isRequired + ? `RCTBridgingTo${type}(${args})` + : `RCTBridgingToOptional${type}(${args})`; + }; + switch (typeAnnotation.type) { + case 'ReservedTypeAnnotation': + switch (typeAnnotation.name) { + case 'RootTag': + return RCTBridgingTo('Double'); + default: + typeAnnotation.name; + throw new Error( + `Couldn't convert into ObjC type: ${typeAnnotation.type}"`, + ); + } + case 'StringTypeAnnotation': + return RCTBridgingTo('String'); + case 'NumberTypeAnnotation': + return RCTBridgingTo('Double'); + case 'FloatTypeAnnotation': + return RCTBridgingTo('Double'); + case 'Int32TypeAnnotation': + return RCTBridgingTo('Double'); + case 'DoubleTypeAnnotation': + return RCTBridgingTo('Double'); + case 'BooleanTypeAnnotation': + return RCTBridgingTo('Bool'); + case 'EnumDeclaration': + switch (typeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return RCTBridgingTo('Double'); + case 'StringTypeAnnotation': + return RCTBridgingTo('String'); + default: + throw new Error( + `Couldn't convert enum into ObjC value: ${typeAnnotation.type}"`, + ); + } + case 'GenericObjectTypeAnnotation': + return value; + case 'ArrayTypeAnnotation': + const elementType = typeAnnotation.elementType; + if (elementType == null) { + return value; + } + const localVarName = `itemValue_${depth}`; + const elementObjCType = toObjCType(hasteModuleName, elementType); + const elementObjCValue = toObjCValue( + hasteModuleName, + elementType, + localVarName, + depth + 1, + ); + return RCTBridgingTo( + 'Vec', + `^${elementObjCType}(id ${localVarName}) { return ${elementObjCValue}; }`, + ); + case 'TypeAliasTypeAnnotation': + const structName = capitalize(typeAnnotation.name); + const namespacedStructName = getNamespacedStructName( + hasteModuleName, + structName, + ); + return !isRequired + ? `(${value} == nil ? std::nullopt : std::make_optional(${namespacedStructName}(${value})))` + : `${namespacedStructName}(${value})`; + default: + typeAnnotation.type; + throw new Error( + `Couldn't convert into ObjC value: ${typeAnnotation.type}"`, + ); + } +} +function serializeRegularStruct(hasteModuleName, struct) { + const declaration = StructTemplate({ + hasteModuleName: hasteModuleName, + structName: struct.name, + structProperties: struct.properties + .map(property => { + const typeAnnotation = property.typeAnnotation, + optional = property.optional; + const safePropName = getSafePropertyName(property); + const returnType = toObjCType( + hasteModuleName, + typeAnnotation, + optional, + ); + const padding = ' '.repeat(returnType.endsWith('*') ? 0 : 1); + return `${returnType}${padding}${safePropName}() const;`; + }) + .join('\n '), + }); + + // $FlowFixMe[missing-type-arg] + const methods = struct.properties + .map(property => { + const typeAnnotation = property.typeAnnotation, + optional = property.optional, + propName = property.name; + const safePropertyName = getSafePropertyName(property); + const returnType = toObjCType(hasteModuleName, typeAnnotation, optional); + const returnValue = toObjCValue( + hasteModuleName, + typeAnnotation, + 'p', + 0, + optional, + ); + const padding = ' '.repeat(returnType.endsWith('*') ? 0 : 1); + return MethodTemplate({ + hasteModuleName, + structName: struct.name, + returnType: returnType + padding, + returnValue: returnValue, + propertyName: propName, + safePropertyName, + }); + }) + .join('\n'); + return { + methods, + declaration, + }; +} +module.exports = { + serializeRegularStruct, +}; diff --git a/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/header/serializeRegularStruct.js.flow b/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/header/serializeRegularStruct.js.flow new file mode 100644 index 00000000000000..9ea93a1da7a192 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/header/serializeRegularStruct.js.flow @@ -0,0 +1,280 @@ +/** + * 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 {Nullable} from '../../../../CodegenSchema'; +import type {RegularStruct, StructTypeAnnotation} from '../StructCollector'; +import type {StructSerilizationOutput} from './serializeStruct'; + +const {unwrapNullable} = require('../../../../parsers/parsers-commons'); +const {wrapOptional: wrapCxxOptional} = require('../../../TypeUtils/Cxx'); +const { + wrapOptional: wrapObjCOptional, +} = require('../../../TypeUtils/Objective-C'); +const {capitalize} = require('../../../Utils'); +const {getNamespacedStructName, getSafePropertyName} = require('../Utils'); + +const StructTemplate = ({ + hasteModuleName, + structName, + structProperties, +}: $ReadOnly<{ + hasteModuleName: string, + structName: string, + structProperties: string, +}>) => `namespace JS { + namespace ${hasteModuleName} { + struct ${structName} { + ${structProperties} + + ${structName}(NSDictionary *const v) : _v(v) {} + private: + NSDictionary *_v; + }; + } +} + +@interface RCTCxxConvert (${hasteModuleName}_${structName}) ++ (RCTManagedPointer *)JS_${hasteModuleName}_${structName}:(id)json; +@end`; + +const MethodTemplate = ({ + returnType, + returnValue, + hasteModuleName, + structName, + propertyName, + safePropertyName, +}: $ReadOnly<{ + returnType: string, + returnValue: string, + hasteModuleName: string, + structName: string, + propertyName: string, + safePropertyName: string, +}>) => `inline ${returnType}JS::${hasteModuleName}::${structName}::${safePropertyName}() const +{ + id const p = _v[@"${propertyName}"]; + return ${returnValue}; +}`; + +function toObjCType( + hasteModuleName: string, + nullableTypeAnnotation: Nullable, + isOptional: boolean = false, +): string { + const [typeAnnotation, nullable] = unwrapNullable(nullableTypeAnnotation); + const isRequired = !nullable && !isOptional; + + switch (typeAnnotation.type) { + case 'ReservedTypeAnnotation': + switch (typeAnnotation.name) { + case 'RootTag': + return wrapCxxOptional('double', isRequired); + default: + (typeAnnotation.name: empty); + throw new Error(`Unknown prop type, found: ${typeAnnotation.name}"`); + } + case 'StringTypeAnnotation': + return 'NSString *'; + case 'NumberTypeAnnotation': + return wrapCxxOptional('double', isRequired); + case 'FloatTypeAnnotation': + return wrapCxxOptional('double', isRequired); + case 'Int32TypeAnnotation': + return wrapCxxOptional('double', isRequired); + case 'DoubleTypeAnnotation': + return wrapCxxOptional('double', isRequired); + case 'BooleanTypeAnnotation': + return wrapCxxOptional('bool', isRequired); + case 'EnumDeclaration': + switch (typeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return wrapCxxOptional('double', isRequired); + case 'StringTypeAnnotation': + return 'NSString *'; + default: + throw new Error( + `Couldn't convert enum into ObjC type: ${typeAnnotation.type}"`, + ); + } + case 'GenericObjectTypeAnnotation': + return wrapObjCOptional('id', isRequired); + case 'ArrayTypeAnnotation': + if (typeAnnotation.elementType == null) { + return wrapObjCOptional('id', isRequired); + } + return wrapCxxOptional( + `facebook::react::LazyVector<${toObjCType( + hasteModuleName, + typeAnnotation.elementType, + )}>`, + isRequired, + ); + case 'TypeAliasTypeAnnotation': + const structName = capitalize(typeAnnotation.name); + const namespacedStructName = getNamespacedStructName( + hasteModuleName, + structName, + ); + return wrapCxxOptional(namespacedStructName, isRequired); + default: + (typeAnnotation.type: empty); + throw new Error( + `Couldn't convert into ObjC type: ${typeAnnotation.type}"`, + ); + } +} + +function toObjCValue( + hasteModuleName: string, + nullableTypeAnnotation: Nullable, + value: string, + depth: number, + isOptional: boolean = false, +): string { + const [typeAnnotation, nullable] = unwrapNullable(nullableTypeAnnotation); + const isRequired = !nullable && !isOptional; + const RCTBridgingTo = (type: string, arg?: string) => { + const args = [value, arg].filter(Boolean).join(', '); + return isRequired + ? `RCTBridgingTo${type}(${args})` + : `RCTBridgingToOptional${type}(${args})`; + }; + + switch (typeAnnotation.type) { + case 'ReservedTypeAnnotation': + switch (typeAnnotation.name) { + case 'RootTag': + return RCTBridgingTo('Double'); + default: + (typeAnnotation.name: empty); + throw new Error( + `Couldn't convert into ObjC type: ${typeAnnotation.type}"`, + ); + } + case 'StringTypeAnnotation': + return RCTBridgingTo('String'); + case 'NumberTypeAnnotation': + return RCTBridgingTo('Double'); + case 'FloatTypeAnnotation': + return RCTBridgingTo('Double'); + case 'Int32TypeAnnotation': + return RCTBridgingTo('Double'); + case 'DoubleTypeAnnotation': + return RCTBridgingTo('Double'); + case 'BooleanTypeAnnotation': + return RCTBridgingTo('Bool'); + case 'EnumDeclaration': + switch (typeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return RCTBridgingTo('Double'); + case 'StringTypeAnnotation': + return RCTBridgingTo('String'); + default: + throw new Error( + `Couldn't convert enum into ObjC value: ${typeAnnotation.type}"`, + ); + } + case 'GenericObjectTypeAnnotation': + return value; + case 'ArrayTypeAnnotation': + const {elementType} = typeAnnotation; + if (elementType == null) { + return value; + } + + const localVarName = `itemValue_${depth}`; + const elementObjCType = toObjCType(hasteModuleName, elementType); + const elementObjCValue = toObjCValue( + hasteModuleName, + elementType, + localVarName, + depth + 1, + ); + + return RCTBridgingTo( + 'Vec', + `^${elementObjCType}(id ${localVarName}) { return ${elementObjCValue}; }`, + ); + case 'TypeAliasTypeAnnotation': + const structName = capitalize(typeAnnotation.name); + const namespacedStructName = getNamespacedStructName( + hasteModuleName, + structName, + ); + + return !isRequired + ? `(${value} == nil ? std::nullopt : std::make_optional(${namespacedStructName}(${value})))` + : `${namespacedStructName}(${value})`; + default: + (typeAnnotation.type: empty); + throw new Error( + `Couldn't convert into ObjC value: ${typeAnnotation.type}"`, + ); + } +} + +function serializeRegularStruct( + hasteModuleName: string, + struct: RegularStruct, +): StructSerilizationOutput { + const declaration = StructTemplate({ + hasteModuleName: hasteModuleName, + structName: struct.name, + structProperties: struct.properties + .map(property => { + const {typeAnnotation, optional} = property; + const safePropName = getSafePropertyName(property); + const returnType = toObjCType( + hasteModuleName, + typeAnnotation, + optional, + ); + + const padding = ' '.repeat(returnType.endsWith('*') ? 0 : 1); + return `${returnType}${padding}${safePropName}() const;`; + }) + .join('\n '), + }); + + // $FlowFixMe[missing-type-arg] + const methods = struct.properties + .map(property => { + const {typeAnnotation, optional, name: propName} = property; + const safePropertyName = getSafePropertyName(property); + const returnType = toObjCType(hasteModuleName, typeAnnotation, optional); + const returnValue = toObjCValue( + hasteModuleName, + typeAnnotation, + 'p', + 0, + optional, + ); + + const padding = ' '.repeat(returnType.endsWith('*') ? 0 : 1); + return MethodTemplate({ + hasteModuleName, + structName: struct.name, + returnType: returnType + padding, + returnValue: returnValue, + propertyName: propName, + safePropertyName, + }); + }) + .join('\n'); + + return {methods, declaration}; +} + +module.exports = { + serializeRegularStruct, +}; diff --git a/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/header/serializeStruct.js b/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/header/serializeStruct.js new file mode 100644 index 00000000000000..d1d668af5d3045 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/header/serializeStruct.js @@ -0,0 +1,25 @@ +/** + * 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 _require = require('./serializeConstantsStruct'), + serializeConstantsStruct = _require.serializeConstantsStruct; +const _require2 = require('./serializeRegularStruct'), + serializeRegularStruct = _require2.serializeRegularStruct; +function serializeStruct(hasteModuleName, struct) { + if (struct.context === 'REGULAR') { + return serializeRegularStruct(hasteModuleName, struct); + } + return serializeConstantsStruct(hasteModuleName, struct); +} +module.exports = { + serializeStruct, +}; diff --git a/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/header/serializeStruct.js.flow b/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/header/serializeStruct.js.flow new file mode 100644 index 00000000000000..d5b38f45357a2e --- /dev/null +++ b/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/header/serializeStruct.js.flow @@ -0,0 +1,35 @@ +/** + * 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 {Struct} from '../StructCollector'; + +const {serializeConstantsStruct} = require('./serializeConstantsStruct'); +const {serializeRegularStruct} = require('./serializeRegularStruct'); + +export type StructSerilizationOutput = $ReadOnly<{ + methods: string, + declaration: string, +}>; + +function serializeStruct( + hasteModuleName: string, + struct: Struct, +): StructSerilizationOutput { + if (struct.context === 'REGULAR') { + return serializeRegularStruct(hasteModuleName, struct); + } + return serializeConstantsStruct(hasteModuleName, struct); +} + +module.exports = { + serializeStruct, +}; diff --git a/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/index.js b/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/index.js new file mode 100644 index 00000000000000..0bad19190970f3 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/index.js @@ -0,0 +1,196 @@ +/** + * 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 _require = require('../Utils'), + createAliasResolver = _require.createAliasResolver, + getModules = _require.getModules; +const _require2 = require('./header/serializeStruct'), + serializeStruct = _require2.serializeStruct; +const _require3 = require('./serializeMethod'), + serializeMethod = _require3.serializeMethod; +const _require4 = require('./source/serializeModule'), + serializeModuleSource = _require4.serializeModuleSource; +const _require5 = require('./StructCollector'), + StructCollector = _require5.StructCollector; +const ModuleDeclarationTemplate = ({ + hasteModuleName, + structDeclarations, + protocolMethods, +}) => `${structDeclarations} +@protocol ${hasteModuleName}Spec + +${protocolMethods} + +@end +namespace facebook::react { + /** + * ObjC++ class for module '${hasteModuleName}' + */ + class JSI_EXPORT ${hasteModuleName}SpecJSI : public ObjCTurboModule { + public: + ${hasteModuleName}SpecJSI(const ObjCTurboModule::InitParams ¶ms); + }; +} // namespace facebook::react`; +const HeaderFileTemplate = ({ + headerFileName, + moduleDeclarations, + structInlineMethods, + assumeNonnull, +}) => { + const headerFileNameWithNoExt = headerFileName.replace(/\.h$/, ''); + return ( + `/** + * 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: GenerateModuleObjCpp + * + * We create an umbrella header (and corresponding implementation) here since + * Cxx compilation in BUCK has a limitation: source-code producing genrule()s + * must have a single output. More files => more genrule()s => slower builds. + */ + +#ifndef __cplusplus +#error This file must be compiled as Obj-C++. If you are importing it, you must change your file extension to .mm. +#endif + +// Avoid multiple includes of ${headerFileNameWithNoExt} symbols +#ifndef ${headerFileNameWithNoExt}_H +#define ${headerFileNameWithNoExt}_H + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +` + + (assumeNonnull ? '\nNS_ASSUME_NONNULL_BEGIN\n' : '') + + moduleDeclarations + + '\n' + + structInlineMethods + + (assumeNonnull ? '\nNS_ASSUME_NONNULL_END\n' : '\n') + + `#endif // ${headerFileNameWithNoExt}_H` + + '\n' + ); +}; +const SourceFileTemplate = ({headerFileName, moduleImplementations}) => `/** + * 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: GenerateModuleObjCpp + * + * We create an umbrella header (and corresponding implementation) here since + * Cxx compilation in BUCK has a limitation: source-code producing genrule()s + * must have a single output. More files => more genrule()s => slower builds. + */ + +#import "${headerFileName}" + +${moduleImplementations} +`; +module.exports = { + generate(libraryName, schema, packageName, assumeNonnull, headerPrefix) { + const nativeModules = getModules(schema); + const moduleDeclarations = []; + const structInlineMethods = []; + const moduleImplementations = []; + const hasteModuleNames = Object.keys(nativeModules).sort(); + for (const hasteModuleName of hasteModuleNames) { + const _nativeModules$hasteM = nativeModules[hasteModuleName], + aliasMap = _nativeModules$hasteM.aliasMap, + excludedPlatforms = _nativeModules$hasteM.excludedPlatforms, + spec = _nativeModules$hasteM.spec; + if (excludedPlatforms != null && excludedPlatforms.includes('iOS')) { + continue; + } + const resolveAlias = createAliasResolver(aliasMap); + const structCollector = new StructCollector(); + const methodSerializations = []; + const serializeProperty = property => { + methodSerializations.push( + ...serializeMethod( + hasteModuleName, + property, + structCollector, + resolveAlias, + ), + ); + }; + + /** + * Note: As we serialize NativeModule methods, we insert structs into + * StructCollector, as we encounter them. + */ + spec.methods + .filter(property => property.name !== 'getConstants') + .forEach(serializeProperty); + spec.methods + .filter(property => property.name === 'getConstants') + .forEach(serializeProperty); + const generatedStructs = structCollector.getAllStructs(); + const structStrs = []; + const methodStrs = []; + for (const struct of generatedStructs) { + const _serializeStruct = serializeStruct(hasteModuleName, struct), + methods = _serializeStruct.methods, + declaration = _serializeStruct.declaration; + structStrs.push(declaration); + methodStrs.push(methods); + } + moduleDeclarations.push( + ModuleDeclarationTemplate({ + hasteModuleName: hasteModuleName, + structDeclarations: structStrs.join('\n'), + protocolMethods: methodSerializations + .map(({protocolMethod}) => protocolMethod) + .join('\n'), + }), + ); + structInlineMethods.push(methodStrs.join('\n')); + moduleImplementations.push( + serializeModuleSource( + hasteModuleName, + generatedStructs, + methodSerializations.filter( + ({selector}) => selector !== '@selector(constantsToExport)', + ), + ), + ); + } + const headerFileName = `${libraryName}.h`; + const headerFile = HeaderFileTemplate({ + headerFileName, + moduleDeclarations: moduleDeclarations.join('\n'), + structInlineMethods: structInlineMethods.join('\n'), + assumeNonnull, + }); + const sourceFileName = `${libraryName}-generated.mm`; + const sourceFile = SourceFileTemplate({ + headerFileName, + moduleImplementations: moduleImplementations.join('\n'), + }); + return new Map([ + [headerFileName, headerFile], + [sourceFileName, sourceFile], + ]); + }, +}; diff --git a/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/index.js.flow b/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/index.js.flow new file mode 100644 index 00000000000000..297cc738ded838 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/index.js.flow @@ -0,0 +1,228 @@ +/** + * 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 {NativeModulePropertyShape} from '../../../CodegenSchema'; +import type {SchemaType} from '../../../CodegenSchema'; +import type {MethodSerializationOutput} from './serializeMethod'; + +const {createAliasResolver, getModules} = require('../Utils'); +const {serializeStruct} = require('./header/serializeStruct'); +const {serializeMethod} = require('./serializeMethod'); +const {serializeModuleSource} = require('./source/serializeModule'); +const {StructCollector} = require('./StructCollector'); + +type FilesOutput = Map; + +const ModuleDeclarationTemplate = ({ + hasteModuleName, + structDeclarations, + protocolMethods, +}: $ReadOnly<{ + hasteModuleName: string, + structDeclarations: string, + protocolMethods: string, +}>) => `${structDeclarations} +@protocol ${hasteModuleName}Spec + +${protocolMethods} + +@end +namespace facebook::react { + /** + * ObjC++ class for module '${hasteModuleName}' + */ + class JSI_EXPORT ${hasteModuleName}SpecJSI : public ObjCTurboModule { + public: + ${hasteModuleName}SpecJSI(const ObjCTurboModule::InitParams ¶ms); + }; +} // namespace facebook::react`; + +const HeaderFileTemplate = ({ + headerFileName, + moduleDeclarations, + structInlineMethods, + assumeNonnull, +}: $ReadOnly<{ + headerFileName: string, + moduleDeclarations: string, + structInlineMethods: string, + assumeNonnull: boolean, +}>) => { + const headerFileNameWithNoExt = headerFileName.replace(/\.h$/, ''); + + return ( + `/** + * 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: GenerateModuleObjCpp + * + * We create an umbrella header (and corresponding implementation) here since + * Cxx compilation in BUCK has a limitation: source-code producing genrule()s + * must have a single output. More files => more genrule()s => slower builds. + */ + +#ifndef __cplusplus +#error This file must be compiled as Obj-C++. If you are importing it, you must change your file extension to .mm. +#endif + +// Avoid multiple includes of ${headerFileNameWithNoExt} symbols +#ifndef ${headerFileNameWithNoExt}_H +#define ${headerFileNameWithNoExt}_H + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +` + + (assumeNonnull ? '\nNS_ASSUME_NONNULL_BEGIN\n' : '') + + moduleDeclarations + + '\n' + + structInlineMethods + + (assumeNonnull ? '\nNS_ASSUME_NONNULL_END\n' : '\n') + + `#endif // ${headerFileNameWithNoExt}_H` + + '\n' + ); +}; + +const SourceFileTemplate = ({ + headerFileName, + moduleImplementations, +}: $ReadOnly<{ + headerFileName: string, + moduleImplementations: 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: GenerateModuleObjCpp + * + * We create an umbrella header (and corresponding implementation) here since + * Cxx compilation in BUCK has a limitation: source-code producing genrule()s + * must have a single output. More files => more genrule()s => slower builds. + */ + +#import "${headerFileName}" + +${moduleImplementations} +`; + +module.exports = { + generate( + libraryName: string, + schema: SchemaType, + packageName?: string, + assumeNonnull: boolean, + headerPrefix?: string, + ): FilesOutput { + const nativeModules = getModules(schema); + + const moduleDeclarations: Array = []; + const structInlineMethods: Array = []; + const moduleImplementations: Array = []; + + const hasteModuleNames: Array = Object.keys(nativeModules).sort(); + for (const hasteModuleName of hasteModuleNames) { + const {aliasMap, excludedPlatforms, spec} = + nativeModules[hasteModuleName]; + if (excludedPlatforms != null && excludedPlatforms.includes('iOS')) { + continue; + } + const resolveAlias = createAliasResolver(aliasMap); + const structCollector = new StructCollector(); + + const methodSerializations: Array = []; + const serializeProperty = (property: NativeModulePropertyShape) => { + methodSerializations.push( + ...serializeMethod( + hasteModuleName, + property, + structCollector, + resolveAlias, + ), + ); + }; + + /** + * Note: As we serialize NativeModule methods, we insert structs into + * StructCollector, as we encounter them. + */ + spec.methods + .filter(property => property.name !== 'getConstants') + .forEach(serializeProperty); + spec.methods + .filter(property => property.name === 'getConstants') + .forEach(serializeProperty); + + const generatedStructs = structCollector.getAllStructs(); + const structStrs = []; + const methodStrs = []; + + for (const struct of generatedStructs) { + const {methods, declaration} = serializeStruct(hasteModuleName, struct); + structStrs.push(declaration); + methodStrs.push(methods); + } + + moduleDeclarations.push( + ModuleDeclarationTemplate({ + hasteModuleName: hasteModuleName, + structDeclarations: structStrs.join('\n'), + protocolMethods: methodSerializations + .map(({protocolMethod}) => protocolMethod) + .join('\n'), + }), + ); + + structInlineMethods.push(methodStrs.join('\n')); + + moduleImplementations.push( + serializeModuleSource( + hasteModuleName, + generatedStructs, + methodSerializations.filter( + ({selector}) => selector !== '@selector(constantsToExport)', + ), + ), + ); + } + + const headerFileName = `${libraryName}.h`; + const headerFile = HeaderFileTemplate({ + headerFileName, + moduleDeclarations: moduleDeclarations.join('\n'), + structInlineMethods: structInlineMethods.join('\n'), + assumeNonnull, + }); + + const sourceFileName = `${libraryName}-generated.mm`; + const sourceFile = SourceFileTemplate({ + headerFileName, + moduleImplementations: moduleImplementations.join('\n'), + }); + + return new Map([ + [headerFileName, headerFile], + [sourceFileName, sourceFile], + ]); + }, +}; diff --git a/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/serializeMethod.js b/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/serializeMethod.js new file mode 100644 index 00000000000000..21e7882d7d94ab --- /dev/null +++ b/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/serializeMethod.js @@ -0,0 +1,539 @@ +/** + * 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'; + +function _slicedToArray(r, e) { + return ( + _arrayWithHoles(r) || + _iterableToArrayLimit(r, e) || + _unsupportedIterableToArray(r, e) || + _nonIterableRest() + ); +} +function _nonIterableRest() { + throw new TypeError( + 'Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.', + ); +} +function _unsupportedIterableToArray(r, a) { + if (r) { + if ('string' == typeof r) return _arrayLikeToArray(r, a); + var t = {}.toString.call(r).slice(8, -1); + return ( + 'Object' === t && r.constructor && (t = r.constructor.name), + 'Map' === t || 'Set' === t + ? Array.from(r) + : 'Arguments' === t || + /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) + ? _arrayLikeToArray(r, a) + : void 0 + ); + } +} +function _arrayLikeToArray(r, a) { + (null == a || a > r.length) && (a = r.length); + for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; + return n; +} +function _iterableToArrayLimit(r, l) { + var t = + null == r + ? null + : ('undefined' != typeof Symbol && r[Symbol.iterator]) || r['@@iterator']; + if (null != t) { + var e, + n, + i, + u, + a = [], + f = !0, + o = !1; + try { + if (((i = (t = t.call(r)).next), 0 === l)) { + if (Object(t) !== t) return; + f = !1; + } else + for ( + ; + !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); + f = !0 + ); + } catch (r) { + (o = !0), (n = r); + } finally { + try { + if (!f && null != t.return && ((u = t.return()), Object(u) !== u)) + return; + } finally { + if (o) throw n; + } + } + return a; + } +} +function _arrayWithHoles(r) { + if (Array.isArray(r)) return r; +} +const _require = require('../../../parsers/parsers-commons'), + unwrapNullable = _require.unwrapNullable, + wrapNullable = _require.wrapNullable; +const _require2 = require('../../TypeUtils/Objective-C'), + wrapOptional = _require2.wrapOptional; +const _require3 = require('../../Utils'), + capitalize = _require3.capitalize; +const _require4 = require('./Utils'), + getNamespacedStructName = _require4.getNamespacedStructName; +const invariant = require('invariant'); +const ProtocolMethodTemplate = ({returnObjCType, methodName, params}) => + `- (${returnObjCType})${methodName}${params};`; +function serializeMethod( + hasteModuleName, + property, + structCollector, + resolveAlias, +) { + const methodName = property.name, + nullableTypeAnnotation = property.typeAnnotation; + const _unwrapNullable = unwrapNullable(nullableTypeAnnotation), + _unwrapNullable2 = _slicedToArray(_unwrapNullable, 1), + propertyTypeAnnotation = _unwrapNullable2[0]; + const params = propertyTypeAnnotation.params; + if (methodName === 'getConstants') { + return serializeConstantsProtocolMethods( + hasteModuleName, + property, + structCollector, + resolveAlias, + ); + } + const methodParams = []; + const structParamRecords = []; + params.forEach((param, index) => { + const structName = getParamStructName(methodName, param); + const _getParamObjCType = getParamObjCType( + hasteModuleName, + methodName, + param, + structName, + structCollector, + resolveAlias, + ), + objCType = _getParamObjCType.objCType, + isStruct = _getParamObjCType.isStruct; + methodParams.push({ + paramName: param.name, + objCType, + }); + if (isStruct) { + structParamRecords.push({ + paramIndex: index, + structName, + }); + } + }); + + // Unwrap returnTypeAnnotation, so we check if the return type is Promise + // TODO(T76719514): Disallow nullable PromiseTypeAnnotations + const _unwrapNullable3 = unwrapNullable( + propertyTypeAnnotation.returnTypeAnnotation, + ), + _unwrapNullable4 = _slicedToArray(_unwrapNullable3, 1), + returnTypeAnnotation = _unwrapNullable4[0]; + if (returnTypeAnnotation.type === 'PromiseTypeAnnotation') { + methodParams.push( + { + paramName: 'resolve', + objCType: 'RCTPromiseResolveBlock', + }, + { + paramName: 'reject', + objCType: 'RCTPromiseRejectBlock', + }, + ); + } + + /** + * Build Protocol Method + **/ + const returnObjCType = getReturnObjCType( + methodName, + propertyTypeAnnotation.returnTypeAnnotation, + ); + const paddingMax = `- (${returnObjCType})${methodName}`.length; + const objCParams = methodParams.reduce( + ($objCParams, {objCType, paramName}, i) => { + const rhs = `(${objCType})${paramName}`; + const padding = ' '.repeat(Math.max(0, paddingMax - paramName.length)); + return i === 0 + ? `:${rhs}` + : `${$objCParams}\n${padding}${paramName}:${rhs}`; + }, + '', + ); + const protocolMethod = ProtocolMethodTemplate({ + methodName, + returnObjCType, + params: objCParams, + }); + + /** + * Build ObjC Selector + */ + // $FlowFixMe[missing-type-arg] + const selector = methodParams + .map(({paramName}) => paramName) + .reduce(($selector, paramName, i) => { + return i === 0 ? `${$selector}:` : `${$selector}${paramName}:`; + }, methodName); + + /** + * Build JS Return type + */ + const returnJSType = getReturnJSType(methodName, returnTypeAnnotation); + return [ + { + methodName, + protocolMethod, + selector: `@selector(${selector})`, + structParamRecords, + returnJSType, + argCount: params.length, + }, + ]; +} +function getParamStructName(methodName, param) { + const _unwrapNullable5 = unwrapNullable(param.typeAnnotation), + _unwrapNullable6 = _slicedToArray(_unwrapNullable5, 1), + typeAnnotation = _unwrapNullable6[0]; + if (typeAnnotation.type === 'TypeAliasTypeAnnotation') { + return typeAnnotation.name; + } + return `Spec${capitalize(methodName)}${capitalize(param.name)}`; +} +function getParamObjCType( + hasteModuleName, + methodName, + param, + structName, + structCollector, + resolveAlias, +) { + const paramName = param.name, + nullableTypeAnnotation = param.typeAnnotation; + const _unwrapNullable7 = unwrapNullable(nullableTypeAnnotation), + _unwrapNullable8 = _slicedToArray(_unwrapNullable7, 2), + typeAnnotation = _unwrapNullable8[0], + nullable = _unwrapNullable8[1]; + const isRequired = !param.optional && !nullable; + const isStruct = objCType => ({ + isStruct: true, + objCType, + }); + const notStruct = objCType => ({ + isStruct: false, + objCType, + }); + + // Handle types that can only be in parameters + switch (typeAnnotation.type) { + case 'FunctionTypeAnnotation': { + return notStruct('RCTResponseSenderBlock'); + } + case 'ArrayTypeAnnotation': { + /** + * Array in params always codegen NSArray * + * + * TODO(T73933406): Support codegen for Arrays of structs and primitives + * + * For example: + * Array => NSArray + * type Animal = {}; + * Array => NSArray, etc. + */ + return notStruct(wrapOptional('NSArray *', !nullable)); + } + } + const _unwrapNullable9 = unwrapNullable( + structCollector.process( + structName, + 'REGULAR', + resolveAlias, + wrapNullable(nullable, typeAnnotation), + ), + ), + _unwrapNullable10 = _slicedToArray(_unwrapNullable9, 1), + structTypeAnnotation = _unwrapNullable10[0]; + invariant( + structTypeAnnotation.type !== 'ArrayTypeAnnotation', + 'ArrayTypeAnnotations should have been processed earlier', + ); + switch (structTypeAnnotation.type) { + case 'TypeAliasTypeAnnotation': { + /** + * TODO(T73943261): Support nullable object literals and aliases? + */ + return isStruct( + getNamespacedStructName(hasteModuleName, structTypeAnnotation.name) + + ' &', + ); + } + case 'ReservedTypeAnnotation': + switch (structTypeAnnotation.name) { + case 'RootTag': + return notStruct(isRequired ? 'double' : 'NSNumber *'); + default: + structTypeAnnotation.name; + throw new Error( + `Unsupported type for param "${paramName}" in ${methodName}. Found: ${structTypeAnnotation.type}`, + ); + } + case 'StringTypeAnnotation': + return notStruct(wrapOptional('NSString *', !nullable)); + case 'NumberTypeAnnotation': + return notStruct(isRequired ? 'double' : 'NSNumber *'); + case 'FloatTypeAnnotation': + return notStruct(isRequired ? 'float' : 'NSNumber *'); + case 'DoubleTypeAnnotation': + return notStruct(isRequired ? 'double' : 'NSNumber *'); + case 'Int32TypeAnnotation': + return notStruct(isRequired ? 'NSInteger' : 'NSNumber *'); + case 'BooleanTypeAnnotation': + return notStruct(isRequired ? 'BOOL' : 'NSNumber *'); + case 'EnumDeclaration': + switch (typeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return notStruct(isRequired ? 'double' : 'NSNumber *'); + case 'StringTypeAnnotation': + return notStruct(wrapOptional('NSString *', !nullable)); + default: + throw new Error( + `Unsupported enum type for param "${paramName}" in ${methodName}. Found: ${typeAnnotation.type}`, + ); + } + case 'GenericObjectTypeAnnotation': + return notStruct(wrapOptional('NSDictionary *', !nullable)); + default: + structTypeAnnotation.type; + throw new Error( + `Unsupported type for param "${paramName}" in ${methodName}. Found: ${typeAnnotation.type}`, + ); + } +} +function getReturnObjCType(methodName, nullableTypeAnnotation) { + const _unwrapNullable11 = unwrapNullable(nullableTypeAnnotation), + _unwrapNullable12 = _slicedToArray(_unwrapNullable11, 2), + typeAnnotation = _unwrapNullable12[0], + nullable = _unwrapNullable12[1]; + const isRequired = !nullable; + switch (typeAnnotation.type) { + case 'VoidTypeAnnotation': + return 'void'; + case 'PromiseTypeAnnotation': + return 'void'; + case 'ObjectTypeAnnotation': + return wrapOptional('NSDictionary *', isRequired); + case 'TypeAliasTypeAnnotation': + return wrapOptional('NSDictionary *', isRequired); + case 'ArrayTypeAnnotation': + if (typeAnnotation.elementType == null) { + return wrapOptional('NSArray> *', isRequired); + } + return wrapOptional( + `NSArray<${getReturnObjCType( + methodName, + typeAnnotation.elementType, + )}> *`, + isRequired, + ); + case 'ReservedTypeAnnotation': + switch (typeAnnotation.name) { + case 'RootTag': + return wrapOptional('NSNumber *', isRequired); + default: + typeAnnotation.name; + throw new Error( + `Unsupported return type for ${methodName}. Found: ${typeAnnotation.name}`, + ); + } + case 'StringTypeAnnotation': + // TODO: Can NSString * returns not be _Nullable? + // In the legacy codegen, we don't surround NSSTring * with _Nullable + return wrapOptional('NSString *', isRequired); + case 'NumberTypeAnnotation': + return wrapOptional('NSNumber *', isRequired); + case 'FloatTypeAnnotation': + return wrapOptional('NSNumber *', isRequired); + case 'DoubleTypeAnnotation': + return wrapOptional('NSNumber *', isRequired); + case 'Int32TypeAnnotation': + return wrapOptional('NSNumber *', isRequired); + case 'BooleanTypeAnnotation': + return wrapOptional('NSNumber *', isRequired); + case 'EnumDeclaration': + switch (typeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return wrapOptional('NSNumber *', isRequired); + case 'StringTypeAnnotation': + return wrapOptional('NSString *', isRequired); + default: + throw new Error( + `Unsupported enum return type for ${methodName}. Found: ${typeAnnotation.type}`, + ); + } + case 'UnionTypeAnnotation': + switch (typeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return wrapOptional('NSNumber *', isRequired); + case 'ObjectTypeAnnotation': + return wrapOptional('NSDictionary *', isRequired); + case 'StringTypeAnnotation': + // TODO: Can NSString * returns not be _Nullable? + // In the legacy codegen, we don't surround NSSTring * with _Nullable + return wrapOptional('NSString *', isRequired); + default: + throw new Error( + `Unsupported union return type for ${methodName}, found: ${typeAnnotation.memberType}"`, + ); + } + case 'GenericObjectTypeAnnotation': + return wrapOptional('NSDictionary *', isRequired); + default: + typeAnnotation.type; + throw new Error( + `Unsupported return type for ${methodName}. Found: ${typeAnnotation.type}`, + ); + } +} +function getReturnJSType(methodName, nullableTypeAnnotation) { + const _unwrapNullable13 = unwrapNullable(nullableTypeAnnotation), + _unwrapNullable14 = _slicedToArray(_unwrapNullable13, 1), + typeAnnotation = _unwrapNullable14[0]; + switch (typeAnnotation.type) { + case 'VoidTypeAnnotation': + return 'VoidKind'; + case 'PromiseTypeAnnotation': + return 'PromiseKind'; + case 'ObjectTypeAnnotation': + return 'ObjectKind'; + case 'TypeAliasTypeAnnotation': + return 'ObjectKind'; + case 'ArrayTypeAnnotation': + return 'ArrayKind'; + case 'ReservedTypeAnnotation': + return 'NumberKind'; + case 'StringTypeAnnotation': + return 'StringKind'; + case 'NumberTypeAnnotation': + return 'NumberKind'; + case 'FloatTypeAnnotation': + return 'NumberKind'; + case 'DoubleTypeAnnotation': + return 'NumberKind'; + case 'Int32TypeAnnotation': + return 'NumberKind'; + case 'BooleanTypeAnnotation': + return 'BooleanKind'; + case 'GenericObjectTypeAnnotation': + return 'ObjectKind'; + case 'EnumDeclaration': + switch (typeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return 'NumberKind'; + case 'StringTypeAnnotation': + return 'StringKind'; + default: + throw new Error( + `Unsupported return type for ${methodName}. Found: ${typeAnnotation.type}`, + ); + } + case 'UnionTypeAnnotation': + switch (typeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return 'NumberKind'; + case 'ObjectTypeAnnotation': + return 'ObjectKind'; + case 'StringTypeAnnotation': + return 'StringKind'; + default: + throw new Error( + `Unsupported return type for ${methodName}. Found: ${typeAnnotation.type}`, + ); + } + default: + typeAnnotation.type; + throw new Error( + `Unsupported return type for ${methodName}. Found: ${typeAnnotation.type}`, + ); + } +} +function serializeConstantsProtocolMethods( + hasteModuleName, + property, + structCollector, + resolveAlias, +) { + const _unwrapNullable15 = unwrapNullable(property.typeAnnotation), + _unwrapNullable16 = _slicedToArray(_unwrapNullable15, 1), + propertyTypeAnnotation = _unwrapNullable16[0]; + if (propertyTypeAnnotation.params.length !== 0) { + throw new Error( + `${hasteModuleName}.getConstants() may only accept 0 arguments.`, + ); + } + let returnTypeAnnotation = propertyTypeAnnotation.returnTypeAnnotation; + if (returnTypeAnnotation.type === 'TypeAliasTypeAnnotation') { + // The return type is an alias, resolve it to get the expected undelying object literal type + returnTypeAnnotation = resolveAlias(returnTypeAnnotation.name); + } + if (returnTypeAnnotation.type !== 'ObjectTypeAnnotation') { + throw new Error( + `${hasteModuleName}.getConstants() may only return an object literal: {...}` + + ` or a type alias of such. Got '${propertyTypeAnnotation.returnTypeAnnotation.type}'.`, + ); + } + if ( + returnTypeAnnotation.type === 'ObjectTypeAnnotation' && + returnTypeAnnotation.properties.length === 0 + ) { + return []; + } + const realTypeAnnotation = structCollector.process( + 'Constants', + 'CONSTANTS', + resolveAlias, + returnTypeAnnotation, + ); + invariant( + realTypeAnnotation.type === 'TypeAliasTypeAnnotation', + "Unable to generate C++ struct from module's getConstants() method return type.", + ); + const returnObjCType = `facebook::react::ModuleConstants`; + + // $FlowFixMe[missing-type-arg] + return ['constantsToExport', 'getConstants'].map(methodName => { + const protocolMethod = ProtocolMethodTemplate({ + methodName, + returnObjCType, + params: '', + }); + return { + methodName, + protocolMethod, + returnJSType: 'ObjectKind', + selector: `@selector(${methodName})`, + structParamRecords: [], + argCount: 0, + }; + }); +} +module.exports = { + serializeMethod, +}; diff --git a/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/serializeMethod.js.flow b/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/serializeMethod.js.flow new file mode 100644 index 00000000000000..14f0ed724c78d4 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/serializeMethod.js.flow @@ -0,0 +1,514 @@ +/** + * 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 { + NamedShape, + NativeModuleParamTypeAnnotation, + NativeModulePropertyShape, + NativeModuleReturnTypeAnnotation, + Nullable, +} from '../../../CodegenSchema'; +import type {AliasResolver} from '../Utils'; +import type {StructCollector} from './StructCollector'; + +const { + unwrapNullable, + wrapNullable, +} = require('../../../parsers/parsers-commons'); +const {wrapOptional} = require('../../TypeUtils/Objective-C'); +const {capitalize} = require('../../Utils'); +const {getNamespacedStructName} = require('./Utils'); +const invariant = require('invariant'); + +const ProtocolMethodTemplate = ({ + returnObjCType, + methodName, + params, +}: $ReadOnly<{ + returnObjCType: string, + methodName: string, + params: string, +}>) => `- (${returnObjCType})${methodName}${params};`; + +export type StructParameterRecord = $ReadOnly<{ + paramIndex: number, + structName: string, +}>; + +type ReturnJSType = + | 'VoidKind' + | 'BooleanKind' + | 'PromiseKind' + | 'ObjectKind' + | 'ArrayKind' + | 'NumberKind' + | 'StringKind'; + +export type MethodSerializationOutput = $ReadOnly<{ + methodName: string, + protocolMethod: string, + selector: string, + structParamRecords: $ReadOnlyArray, + returnJSType: ReturnJSType, + argCount: number, +}>; + +function serializeMethod( + hasteModuleName: string, + property: NativeModulePropertyShape, + structCollector: StructCollector, + resolveAlias: AliasResolver, +): $ReadOnlyArray { + const {name: methodName, typeAnnotation: nullableTypeAnnotation} = property; + const [propertyTypeAnnotation] = unwrapNullable(nullableTypeAnnotation); + const {params} = propertyTypeAnnotation; + + if (methodName === 'getConstants') { + return serializeConstantsProtocolMethods( + hasteModuleName, + property, + structCollector, + resolveAlias, + ); + } + + const methodParams: Array<{paramName: string, objCType: string}> = []; + const structParamRecords: Array = []; + + params.forEach((param, index) => { + const structName = getParamStructName(methodName, param); + const {objCType, isStruct} = getParamObjCType( + hasteModuleName, + methodName, + param, + structName, + structCollector, + resolveAlias, + ); + + methodParams.push({paramName: param.name, objCType}); + + if (isStruct) { + structParamRecords.push({paramIndex: index, structName}); + } + }); + + // Unwrap returnTypeAnnotation, so we check if the return type is Promise + // TODO(T76719514): Disallow nullable PromiseTypeAnnotations + const [returnTypeAnnotation] = unwrapNullable( + propertyTypeAnnotation.returnTypeAnnotation, + ); + + if (returnTypeAnnotation.type === 'PromiseTypeAnnotation') { + methodParams.push( + {paramName: 'resolve', objCType: 'RCTPromiseResolveBlock'}, + {paramName: 'reject', objCType: 'RCTPromiseRejectBlock'}, + ); + } + + /** + * Build Protocol Method + **/ + const returnObjCType = getReturnObjCType( + methodName, + propertyTypeAnnotation.returnTypeAnnotation, + ); + const paddingMax = `- (${returnObjCType})${methodName}`.length; + + const objCParams = methodParams.reduce( + ($objCParams, {objCType, paramName}, i) => { + const rhs = `(${objCType})${paramName}`; + const padding = ' '.repeat(Math.max(0, paddingMax - paramName.length)); + return i === 0 + ? `:${rhs}` + : `${$objCParams}\n${padding}${paramName}:${rhs}`; + }, + '', + ); + + const protocolMethod = ProtocolMethodTemplate({ + methodName, + returnObjCType, + params: objCParams, + }); + + /** + * Build ObjC Selector + */ + // $FlowFixMe[missing-type-arg] + const selector = methodParams + .map(({paramName}) => paramName) + .reduce(($selector, paramName, i) => { + return i === 0 ? `${$selector}:` : `${$selector}${paramName}:`; + }, methodName); + + /** + * Build JS Return type + */ + const returnJSType = getReturnJSType(methodName, returnTypeAnnotation); + + return [ + { + methodName, + protocolMethod, + selector: `@selector(${selector})`, + structParamRecords, + returnJSType, + argCount: params.length, + }, + ]; +} + +type Param = NamedShape>; + +function getParamStructName(methodName: string, param: Param): string { + const [typeAnnotation] = unwrapNullable(param.typeAnnotation); + if (typeAnnotation.type === 'TypeAliasTypeAnnotation') { + return typeAnnotation.name; + } + + return `Spec${capitalize(methodName)}${capitalize(param.name)}`; +} + +function getParamObjCType( + hasteModuleName: string, + methodName: string, + param: Param, + structName: string, + structCollector: StructCollector, + resolveAlias: AliasResolver, +): $ReadOnly<{objCType: string, isStruct: boolean}> { + const {name: paramName, typeAnnotation: nullableTypeAnnotation} = param; + const [typeAnnotation, nullable] = unwrapNullable(nullableTypeAnnotation); + const isRequired = !param.optional && !nullable; + + const isStruct = (objCType: string) => ({ + isStruct: true, + objCType, + }); + + const notStruct = (objCType: string) => ({ + isStruct: false, + objCType, + }); + + // Handle types that can only be in parameters + switch (typeAnnotation.type) { + case 'FunctionTypeAnnotation': { + return notStruct('RCTResponseSenderBlock'); + } + case 'ArrayTypeAnnotation': { + /** + * Array in params always codegen NSArray * + * + * TODO(T73933406): Support codegen for Arrays of structs and primitives + * + * For example: + * Array => NSArray + * type Animal = {}; + * Array => NSArray, etc. + */ + return notStruct(wrapOptional('NSArray *', !nullable)); + } + } + + const [structTypeAnnotation] = unwrapNullable( + structCollector.process( + structName, + 'REGULAR', + resolveAlias, + wrapNullable(nullable, typeAnnotation), + ), + ); + + invariant( + structTypeAnnotation.type !== 'ArrayTypeAnnotation', + 'ArrayTypeAnnotations should have been processed earlier', + ); + + switch (structTypeAnnotation.type) { + case 'TypeAliasTypeAnnotation': { + /** + * TODO(T73943261): Support nullable object literals and aliases? + */ + return isStruct( + getNamespacedStructName(hasteModuleName, structTypeAnnotation.name) + + ' &', + ); + } + case 'ReservedTypeAnnotation': + switch (structTypeAnnotation.name) { + case 'RootTag': + return notStruct(isRequired ? 'double' : 'NSNumber *'); + default: + (structTypeAnnotation.name: empty); + throw new Error( + `Unsupported type for param "${paramName}" in ${methodName}. Found: ${structTypeAnnotation.type}`, + ); + } + case 'StringTypeAnnotation': + return notStruct(wrapOptional('NSString *', !nullable)); + case 'NumberTypeAnnotation': + return notStruct(isRequired ? 'double' : 'NSNumber *'); + case 'FloatTypeAnnotation': + return notStruct(isRequired ? 'float' : 'NSNumber *'); + case 'DoubleTypeAnnotation': + return notStruct(isRequired ? 'double' : 'NSNumber *'); + case 'Int32TypeAnnotation': + return notStruct(isRequired ? 'NSInteger' : 'NSNumber *'); + case 'BooleanTypeAnnotation': + return notStruct(isRequired ? 'BOOL' : 'NSNumber *'); + case 'EnumDeclaration': + switch (typeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return notStruct(isRequired ? 'double' : 'NSNumber *'); + case 'StringTypeAnnotation': + return notStruct(wrapOptional('NSString *', !nullable)); + default: + throw new Error( + `Unsupported enum type for param "${paramName}" in ${methodName}. Found: ${typeAnnotation.type}`, + ); + } + case 'GenericObjectTypeAnnotation': + return notStruct(wrapOptional('NSDictionary *', !nullable)); + default: + (structTypeAnnotation.type: empty); + throw new Error( + `Unsupported type for param "${paramName}" in ${methodName}. Found: ${typeAnnotation.type}`, + ); + } +} + +function getReturnObjCType( + methodName: string, + nullableTypeAnnotation: Nullable, +): string { + const [typeAnnotation, nullable] = unwrapNullable(nullableTypeAnnotation); + const isRequired = !nullable; + + switch (typeAnnotation.type) { + case 'VoidTypeAnnotation': + return 'void'; + case 'PromiseTypeAnnotation': + return 'void'; + case 'ObjectTypeAnnotation': + return wrapOptional('NSDictionary *', isRequired); + case 'TypeAliasTypeAnnotation': + return wrapOptional('NSDictionary *', isRequired); + case 'ArrayTypeAnnotation': + if (typeAnnotation.elementType == null) { + return wrapOptional('NSArray> *', isRequired); + } + + return wrapOptional( + `NSArray<${getReturnObjCType( + methodName, + typeAnnotation.elementType, + )}> *`, + isRequired, + ); + case 'ReservedTypeAnnotation': + switch (typeAnnotation.name) { + case 'RootTag': + return wrapOptional('NSNumber *', isRequired); + default: + (typeAnnotation.name: empty); + throw new Error( + `Unsupported return type for ${methodName}. Found: ${typeAnnotation.name}`, + ); + } + case 'StringTypeAnnotation': + // TODO: Can NSString * returns not be _Nullable? + // In the legacy codegen, we don't surround NSSTring * with _Nullable + return wrapOptional('NSString *', isRequired); + case 'NumberTypeAnnotation': + return wrapOptional('NSNumber *', isRequired); + case 'FloatTypeAnnotation': + return wrapOptional('NSNumber *', isRequired); + case 'DoubleTypeAnnotation': + return wrapOptional('NSNumber *', isRequired); + case 'Int32TypeAnnotation': + return wrapOptional('NSNumber *', isRequired); + case 'BooleanTypeAnnotation': + return wrapOptional('NSNumber *', isRequired); + case 'EnumDeclaration': + switch (typeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return wrapOptional('NSNumber *', isRequired); + case 'StringTypeAnnotation': + return wrapOptional('NSString *', isRequired); + default: + throw new Error( + `Unsupported enum return type for ${methodName}. Found: ${typeAnnotation.type}`, + ); + } + case 'UnionTypeAnnotation': + switch (typeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return wrapOptional('NSNumber *', isRequired); + case 'ObjectTypeAnnotation': + return wrapOptional('NSDictionary *', isRequired); + case 'StringTypeAnnotation': + // TODO: Can NSString * returns not be _Nullable? + // In the legacy codegen, we don't surround NSSTring * with _Nullable + return wrapOptional('NSString *', isRequired); + default: + throw new Error( + `Unsupported union return type for ${methodName}, found: ${typeAnnotation.memberType}"`, + ); + } + case 'GenericObjectTypeAnnotation': + return wrapOptional('NSDictionary *', isRequired); + default: + (typeAnnotation.type: 'MixedTypeAnnotation'); + throw new Error( + `Unsupported return type for ${methodName}. Found: ${typeAnnotation.type}`, + ); + } +} + +function getReturnJSType( + methodName: string, + nullableTypeAnnotation: Nullable, +): ReturnJSType { + const [typeAnnotation] = unwrapNullable(nullableTypeAnnotation); + switch (typeAnnotation.type) { + case 'VoidTypeAnnotation': + return 'VoidKind'; + case 'PromiseTypeAnnotation': + return 'PromiseKind'; + case 'ObjectTypeAnnotation': + return 'ObjectKind'; + case 'TypeAliasTypeAnnotation': + return 'ObjectKind'; + case 'ArrayTypeAnnotation': + return 'ArrayKind'; + case 'ReservedTypeAnnotation': + return 'NumberKind'; + case 'StringTypeAnnotation': + return 'StringKind'; + case 'NumberTypeAnnotation': + return 'NumberKind'; + case 'FloatTypeAnnotation': + return 'NumberKind'; + case 'DoubleTypeAnnotation': + return 'NumberKind'; + case 'Int32TypeAnnotation': + return 'NumberKind'; + case 'BooleanTypeAnnotation': + return 'BooleanKind'; + case 'GenericObjectTypeAnnotation': + return 'ObjectKind'; + case 'EnumDeclaration': + switch (typeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return 'NumberKind'; + case 'StringTypeAnnotation': + return 'StringKind'; + default: + throw new Error( + `Unsupported return type for ${methodName}. Found: ${typeAnnotation.type}`, + ); + } + case 'UnionTypeAnnotation': + switch (typeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return 'NumberKind'; + case 'ObjectTypeAnnotation': + return 'ObjectKind'; + case 'StringTypeAnnotation': + return 'StringKind'; + default: + throw new Error( + `Unsupported return type for ${methodName}. Found: ${typeAnnotation.type}`, + ); + } + default: + (typeAnnotation.type: 'MixedTypeAnnotation'); + throw new Error( + `Unsupported return type for ${methodName}. Found: ${typeAnnotation.type}`, + ); + } +} + +function serializeConstantsProtocolMethods( + hasteModuleName: string, + property: NativeModulePropertyShape, + structCollector: StructCollector, + resolveAlias: AliasResolver, +): $ReadOnlyArray { + const [propertyTypeAnnotation] = unwrapNullable(property.typeAnnotation); + if (propertyTypeAnnotation.params.length !== 0) { + throw new Error( + `${hasteModuleName}.getConstants() may only accept 0 arguments.`, + ); + } + + let {returnTypeAnnotation} = propertyTypeAnnotation; + + if (returnTypeAnnotation.type === 'TypeAliasTypeAnnotation') { + // The return type is an alias, resolve it to get the expected undelying object literal type + returnTypeAnnotation = resolveAlias(returnTypeAnnotation.name); + } + + if (returnTypeAnnotation.type !== 'ObjectTypeAnnotation') { + throw new Error( + `${hasteModuleName}.getConstants() may only return an object literal: {...}` + + ` or a type alias of such. Got '${propertyTypeAnnotation.returnTypeAnnotation.type}'.`, + ); + } + + if ( + returnTypeAnnotation.type === 'ObjectTypeAnnotation' && + returnTypeAnnotation.properties.length === 0 + ) { + return []; + } + + const realTypeAnnotation = structCollector.process( + 'Constants', + 'CONSTANTS', + resolveAlias, + returnTypeAnnotation, + ); + + invariant( + realTypeAnnotation.type === 'TypeAliasTypeAnnotation', + "Unable to generate C++ struct from module's getConstants() method return type.", + ); + + const returnObjCType = `facebook::react::ModuleConstants`; + + // $FlowFixMe[missing-type-arg] + return ['constantsToExport', 'getConstants'].map( + methodName => { + const protocolMethod = ProtocolMethodTemplate({ + methodName, + returnObjCType, + params: '', + }); + + return { + methodName, + protocolMethod, + returnJSType: 'ObjectKind', + selector: `@selector(${methodName})`, + structParamRecords: [], + argCount: 0, + }; + }, + ); +} + +module.exports = { + serializeMethod, +}; diff --git a/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/source/serializeModule.js b/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/source/serializeModule.js new file mode 100644 index 00000000000000..66235ca3d8ae30 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/source/serializeModule.js @@ -0,0 +1,94 @@ +/** + * 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 ModuleTemplate = ({ + hasteModuleName, + structs, + methodSerializationOutputs, +}) => `${structs + .map(struct => + RCTCxxConvertCategoryTemplate({ + hasteModuleName, + structName: struct.name, + }), + ) + .join('\n')} +namespace facebook::react { + ${methodSerializationOutputs + .map(serializedMethodParts => + InlineHostFunctionTemplate({ + hasteModuleName, + methodName: serializedMethodParts.methodName, + returnJSType: serializedMethodParts.returnJSType, + selector: serializedMethodParts.selector, + }), + ) + .join('\n')} + + ${hasteModuleName}SpecJSI::${hasteModuleName}SpecJSI(const ObjCTurboModule::InitParams ¶ms) + : ObjCTurboModule(params) { + ${methodSerializationOutputs + .map(({methodName, structParamRecords, argCount}) => + MethodMapEntryTemplate({ + hasteModuleName, + methodName, + structParamRecords, + argCount, + }), + ) + .join('\n' + ' '.repeat(8))} + } +} // namespace facebook::react`; +const RCTCxxConvertCategoryTemplate = ({ + hasteModuleName, + structName, +}) => `@implementation RCTCxxConvert (${hasteModuleName}_${structName}) ++ (RCTManagedPointer *)JS_${hasteModuleName}_${structName}:(id)json +{ + return facebook::react::managedPointer(json); +} +@end`; +const InlineHostFunctionTemplate = ({ + hasteModuleName, + methodName, + returnJSType, + selector, +}) => ` + static facebook::jsi::Value __hostFunction_${hasteModuleName}SpecJSI_${methodName}(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeObjCMethod(rt, ${returnJSType}, "${methodName}", ${selector}, args, count); + }`; +const MethodMapEntryTemplate = ({ + hasteModuleName, + methodName, + structParamRecords, + argCount, +}) => ` + methodMap_["${methodName}"] = MethodMetadata {${argCount}, __hostFunction_${hasteModuleName}SpecJSI_${methodName}}; + ${structParamRecords + .map(({paramIndex, structName}) => { + return `setMethodArgConversionSelector(@"${methodName}", ${paramIndex}, @"JS_${hasteModuleName}_${structName}:");`; + }) + .join('\n' + ' '.repeat(8))}`; +function serializeModuleSource( + hasteModuleName, + structs, + methodSerializationOutputs, +) { + return ModuleTemplate({ + hasteModuleName, + structs: structs.filter(({context}) => context !== 'CONSTANTS'), + methodSerializationOutputs, + }); +} +module.exports = { + serializeModuleSource, +}; diff --git a/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/source/serializeModule.js.flow b/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/source/serializeModule.js.flow new file mode 100644 index 00000000000000..62cc5e204ff088 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/modules/GenerateModuleObjCpp/source/serializeModule.js.flow @@ -0,0 +1,119 @@ +/** + * 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 { + MethodSerializationOutput, + StructParameterRecord, +} from '../serializeMethod'; +import type {Struct} from '../StructCollector'; + +const ModuleTemplate = ({ + hasteModuleName, + structs, + methodSerializationOutputs, +}: $ReadOnly<{ + hasteModuleName: string, + structs: $ReadOnlyArray, + methodSerializationOutputs: $ReadOnlyArray, +}>) => `${structs + .map(struct => + RCTCxxConvertCategoryTemplate({hasteModuleName, structName: struct.name}), + ) + .join('\n')} +namespace facebook::react { + ${methodSerializationOutputs + .map(serializedMethodParts => + InlineHostFunctionTemplate({ + hasteModuleName, + methodName: serializedMethodParts.methodName, + returnJSType: serializedMethodParts.returnJSType, + selector: serializedMethodParts.selector, + }), + ) + .join('\n')} + + ${hasteModuleName}SpecJSI::${hasteModuleName}SpecJSI(const ObjCTurboModule::InitParams ¶ms) + : ObjCTurboModule(params) { + ${methodSerializationOutputs + .map(({methodName, structParamRecords, argCount}) => + MethodMapEntryTemplate({ + hasteModuleName, + methodName, + structParamRecords, + argCount, + }), + ) + .join('\n' + ' '.repeat(8))} + } +} // namespace facebook::react`; + +const RCTCxxConvertCategoryTemplate = ({ + hasteModuleName, + structName, +}: $ReadOnly<{ + hasteModuleName: string, + structName: string, +}>) => `@implementation RCTCxxConvert (${hasteModuleName}_${structName}) ++ (RCTManagedPointer *)JS_${hasteModuleName}_${structName}:(id)json +{ + return facebook::react::managedPointer(json); +} +@end`; + +const InlineHostFunctionTemplate = ({ + hasteModuleName, + methodName, + returnJSType, + selector, +}: $ReadOnly<{ + hasteModuleName: string, + methodName: string, + returnJSType: string, + selector: string, +}>) => ` + static facebook::jsi::Value __hostFunction_${hasteModuleName}SpecJSI_${methodName}(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeObjCMethod(rt, ${returnJSType}, "${methodName}", ${selector}, args, count); + }`; + +const MethodMapEntryTemplate = ({ + hasteModuleName, + methodName, + structParamRecords, + argCount, +}: $ReadOnly<{ + hasteModuleName: string, + methodName: string, + structParamRecords: $ReadOnlyArray, + argCount: number, +}>) => ` + methodMap_["${methodName}"] = MethodMetadata {${argCount}, __hostFunction_${hasteModuleName}SpecJSI_${methodName}}; + ${structParamRecords + .map(({paramIndex, structName}) => { + return `setMethodArgConversionSelector(@"${methodName}", ${paramIndex}, @"JS_${hasteModuleName}_${structName}:");`; + }) + .join('\n' + ' '.repeat(8))}`; + +function serializeModuleSource( + hasteModuleName: string, + structs: $ReadOnlyArray, + methodSerializationOutputs: $ReadOnlyArray, +): string { + return ModuleTemplate({ + hasteModuleName, + structs: structs.filter(({context}) => context !== 'CONSTANTS'), + methodSerializationOutputs, + }); +} + +module.exports = { + serializeModuleSource, +}; diff --git a/packages/react-native-codegen/lib/generators/modules/Utils.js b/packages/react-native-codegen/lib/generators/modules/Utils.js new file mode 100644 index 00000000000000..e047c07b746169 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/modules/Utils.js @@ -0,0 +1,136 @@ +/** + * 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'; + +function _slicedToArray(r, e) { + return ( + _arrayWithHoles(r) || + _iterableToArrayLimit(r, e) || + _unsupportedIterableToArray(r, e) || + _nonIterableRest() + ); +} +function _nonIterableRest() { + throw new TypeError( + 'Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.', + ); +} +function _unsupportedIterableToArray(r, a) { + if (r) { + if ('string' == typeof r) return _arrayLikeToArray(r, a); + var t = {}.toString.call(r).slice(8, -1); + return ( + 'Object' === t && r.constructor && (t = r.constructor.name), + 'Map' === t || 'Set' === t + ? Array.from(r) + : 'Arguments' === t || + /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) + ? _arrayLikeToArray(r, a) + : void 0 + ); + } +} +function _arrayLikeToArray(r, a) { + (null == a || a > r.length) && (a = r.length); + for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; + return n; +} +function _iterableToArrayLimit(r, l) { + var t = + null == r + ? null + : ('undefined' != typeof Symbol && r[Symbol.iterator]) || r['@@iterator']; + if (null != t) { + var e, + n, + i, + u, + a = [], + f = !0, + o = !1; + try { + if (((i = (t = t.call(r)).next), 0 === l)) { + if (Object(t) !== t) return; + f = !1; + } else + for ( + ; + !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); + f = !0 + ); + } catch (r) { + (o = !0), (n = r); + } finally { + try { + if (!f && null != t.return && ((u = t.return()), Object(u) !== u)) + return; + } finally { + if (o) throw n; + } + } + return a; + } +} +function _arrayWithHoles(r) { + if (Array.isArray(r)) return r; +} +const _require = require('../../parsers/parsers-commons'), + unwrapNullable = _require.unwrapNullable; +const invariant = require('invariant'); +function createAliasResolver(aliasMap) { + return aliasName => { + const alias = aliasMap[aliasName]; + invariant(alias != null, `Unable to resolve type alias '${aliasName}'.`); + return alias; + }; +} +function getModules(schema) { + return Object.keys(schema.modules).reduce((modules, hasteModuleName) => { + const module = schema.modules[hasteModuleName]; + if (module == null || module.type === 'Component') { + return modules; + } + modules[hasteModuleName] = module; + return modules; + }, {}); +} +function isDirectRecursiveMember( + parentObjectAliasName, + nullableTypeAnnotation, +) { + const _unwrapNullable = unwrapNullable(nullableTypeAnnotation), + _unwrapNullable2 = _slicedToArray(_unwrapNullable, 1), + typeAnnotation = _unwrapNullable2[0]; + return ( + parentObjectAliasName !== undefined && + typeAnnotation.name === parentObjectAliasName + ); +} +function isArrayRecursiveMember(parentObjectAliasName, nullableTypeAnnotation) { + var _typeAnnotation$eleme; + const _unwrapNullable3 = unwrapNullable(nullableTypeAnnotation), + _unwrapNullable4 = _slicedToArray(_unwrapNullable3, 1), + typeAnnotation = _unwrapNullable4[0]; + return ( + parentObjectAliasName !== undefined && + typeAnnotation.type === 'ArrayTypeAnnotation' && + ((_typeAnnotation$eleme = typeAnnotation.elementType) === null || + _typeAnnotation$eleme === void 0 + ? void 0 + : _typeAnnotation$eleme.name) === parentObjectAliasName + ); +} +module.exports = { + createAliasResolver, + getModules, + isDirectRecursiveMember, + isArrayRecursiveMember, +}; diff --git a/packages/react-native-codegen/lib/generators/modules/Utils.js.flow b/packages/react-native-codegen/lib/generators/modules/Utils.js.flow new file mode 100644 index 00000000000000..efb39462a27576 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/modules/Utils.js.flow @@ -0,0 +1,85 @@ +/** + * 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 { + NativeModuleAliasMap, + NativeModuleObjectTypeAnnotation, + NativeModuleSchema, + NativeModuleTypeAnnotation, + Nullable, + SchemaType, +} from '../../CodegenSchema'; + +const {unwrapNullable} = require('../../parsers/parsers-commons'); +const invariant = require('invariant'); + +export type AliasResolver = ( + aliasName: string, +) => NativeModuleObjectTypeAnnotation; + +function createAliasResolver(aliasMap: NativeModuleAliasMap): AliasResolver { + return (aliasName: string) => { + const alias = aliasMap[aliasName]; + invariant(alias != null, `Unable to resolve type alias '${aliasName}'.`); + return alias; + }; +} + +function getModules( + schema: SchemaType, +): $ReadOnly<{[hasteModuleName: string]: NativeModuleSchema}> { + return Object.keys(schema.modules).reduce<{[string]: NativeModuleSchema}>( + (modules, hasteModuleName: string) => { + const module = schema.modules[hasteModuleName]; + if (module == null || module.type === 'Component') { + return modules; + } + modules[hasteModuleName] = module; + return modules; + }, + {}, + ); +} + +function isDirectRecursiveMember( + parentObjectAliasName: ?string, + nullableTypeAnnotation: Nullable, +): boolean { + const [typeAnnotation] = unwrapNullable( + nullableTypeAnnotation, + ); + return ( + parentObjectAliasName !== undefined && + typeAnnotation.name === parentObjectAliasName + ); +} + +function isArrayRecursiveMember( + parentObjectAliasName: ?string, + nullableTypeAnnotation: Nullable, +): boolean { + const [typeAnnotation] = unwrapNullable( + nullableTypeAnnotation, + ); + return ( + parentObjectAliasName !== undefined && + typeAnnotation.type === 'ArrayTypeAnnotation' && + typeAnnotation.elementType?.name === parentObjectAliasName + ); +} + +module.exports = { + createAliasResolver, + getModules, + isDirectRecursiveMember, + isArrayRecursiveMember, +}; diff --git a/packages/react-native-codegen/lib/generators/modules/__test_fixtures__/fixtures.js b/packages/react-native-codegen/lib/generators/modules/__test_fixtures__/fixtures.js new file mode 100644 index 00000000000000..da08582ee8e278 --- /dev/null +++ b/packages/react-native-codegen/lib/generators/modules/__test_fixtures__/fixtures.js @@ -0,0 +1,2431 @@ +/** + * 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 EMPTY_NATIVE_MODULES = { + modules: { + NativeSampleTurboModule: { + type: 'NativeModule', + aliasMap: {}, + enumMap: {}, + spec: { + eventEmitters: [], + methods: [], + }, + moduleName: 'SampleTurboModule', + }, + }, +}; +const SIMPLE_NATIVE_MODULES = { + modules: { + NativeSampleTurboModule: { + type: 'NativeModule', + aliasMap: {}, + enumMap: { + NumEnum: { + type: 'EnumDeclarationWithMembers', + name: 'NumEnum', + memberType: 'NumberTypeAnnotation', + members: [ + { + name: 'ONE', + value: '1', + }, + { + name: 'TWO', + value: '2', + }, + ], + }, + FloatEnum: { + type: 'EnumDeclarationWithMembers', + name: 'FloatEnum', + memberType: 'NumberTypeAnnotation', + members: [ + { + name: 'POINT_ZERO', + value: '0.0', + }, + { + name: 'POINT_ONE', + value: '0.1', + }, + { + name: 'POINT_TWO', + value: '0.2', + }, + ], + }, + StringEnum: { + type: 'EnumDeclarationWithMembers', + name: 'StringEnum', + memberType: 'StringTypeAnnotation', + members: [ + { + name: 'HELLO', + value: 'hello', + }, + { + name: 'GoodBye', + value: 'goodbye', + }, + ], + }, + }, + spec: { + eventEmitters: [], + methods: [ + { + name: 'getConstants', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [ + { + optional: false, + name: 'const1', + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + { + optional: false, + name: 'const2', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + optional: false, + name: 'const3', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + ], + }, + params: [], + }, + }, + { + name: 'voidFunc', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + params: [], + }, + }, + { + name: 'getBool', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + params: [ + { + optional: false, + name: 'arg', + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'getNumber', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'NumberTypeAnnotation', + }, + params: [ + { + optional: false, + name: 'arg', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'getString', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'StringTypeAnnotation', + }, + params: [ + { + optional: false, + name: 'arg', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'getArray', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'GenericObjectTypeAnnotation', + }, + }, + params: [ + { + name: 'arg', + optional: false, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'GenericObjectTypeAnnotation', + }, + }, + }, + ], + }, + }, + { + name: 'getObject', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'GenericObjectTypeAnnotation', + }, + params: [ + { + optional: false, + name: 'arg', + typeAnnotation: { + type: 'GenericObjectTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'getRootTag', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'ReservedTypeAnnotation', + name: 'RootTag', + }, + params: [ + { + optional: false, + name: 'arg', + typeAnnotation: { + type: 'ReservedTypeAnnotation', + name: 'RootTag', + }, + }, + ], + }, + }, + { + name: 'getValue', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'GenericObjectTypeAnnotation', + }, + params: [ + { + optional: false, + name: 'x', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + optional: false, + name: 'y', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + optional: false, + name: 'z', + typeAnnotation: { + type: 'GenericObjectTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'getEnumReturn', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'EnumDeclaration', + name: 'NumEnum', + memberType: 'NumberTypeAnnotation', + }, + params: [], + }, + }, + { + name: 'getValueWithCallback', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + params: [ + { + name: 'callback', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + params: [], + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + }, + }, + ], + }, + }, + { + name: 'getValueWithPromise', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'PromiseTypeAnnotation', + }, + params: [ + { + optional: false, + name: 'error', + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'getValueWithOptionalArg', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'PromiseTypeAnnotation', + }, + params: [ + { + optional: true, + name: 'parameter', + typeAnnotation: { + type: 'GenericObjectTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'getEnums', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'StringTypeAnnotation', + }, + params: [ + { + name: 'enumInt', + optional: false, + typeAnnotation: { + name: 'NumEnum', + type: 'EnumDeclaration', + memberType: 'NumberTypeAnnotation', + }, + }, + { + name: 'enumFloat', + optional: false, + typeAnnotation: { + name: 'FloatEnum', + type: 'EnumDeclaration', + memberType: 'NumberTypeAnnotation', + }, + }, + { + name: 'enumString', + optional: false, + typeAnnotation: { + name: 'StringEnum', + type: 'EnumDeclaration', + memberType: 'StringTypeAnnotation', + }, + }, + ], + }, + }, + ], + }, + moduleName: 'SampleTurboModule', + }, + }, +}; +const TWO_MODULES_DIFFERENT_FILES = { + modules: { + NativeSampleTurboModule: { + type: 'NativeModule', + aliasMap: {}, + enumMap: {}, + spec: { + eventEmitters: [], + methods: [ + { + name: 'voidFunc', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + params: [], + }, + }, + ], + }, + moduleName: 'SampleTurboModule', + }, + NativeSampleTurboModule2: { + type: 'NativeModule', + aliasMap: {}, + enumMap: {}, + spec: { + eventEmitters: [], + methods: [ + { + name: 'getConstants', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [], + }, + params: [], + }, + }, + { + name: 'voidFunc', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + params: [], + }, + }, + ], + }, + moduleName: 'SampleTurboModule2', + }, + }, +}; +const COMPLEX_OBJECTS = { + modules: { + NativeSampleTurboModule: { + type: 'NativeModule', + aliasMap: {}, + enumMap: {}, + spec: { + eventEmitters: [], + methods: [ + { + name: 'difficult', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [ + { + optional: false, + name: 'D', + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + { + optional: false, + name: 'E', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + optional: false, + name: 'F', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + ], + }, + params: [ + { + optional: false, + name: 'A', + typeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [ + { + optional: false, + name: 'D', + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + { + optional: false, + name: 'E', + typeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [ + { + optional: false, + name: 'D', + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + { + optional: false, + name: 'E', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + optional: false, + name: 'F', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + optional: false, + name: 'id', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + ], + }, + }, + { + optional: false, + name: 'F', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + ], + }, + }, + ], + }, + }, + { + name: 'optionals', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + params: [ + { + optional: false, + name: 'A', + typeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [ + { + optional: true, + name: 'optionalNumberProperty', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + optional: true, + name: 'optionalArrayProperty', + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'NumberTypeAnnotation', + }, + }, + }, + { + optional: true, + name: 'optionalObjectProperty', + typeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [ + { + optional: false, + name: 'x', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + optional: false, + name: 'y', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + ], + }, + }, + { + optional: true, + name: 'optionalGenericObjectProperty', + typeAnnotation: { + type: 'GenericObjectTypeAnnotation', + }, + }, + { + optional: true, + name: 'optionalBooleanTypeProperty', + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + ], + }, + }, + ], + }, + }, + { + name: 'optionalMethod', + optional: true, + typeAnnotation: { + type: 'NullableTypeAnnotation', + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + params: [ + { + optional: false, + name: 'options', + typeAnnotation: { + type: 'GenericObjectTypeAnnotation', + }, + }, + { + name: 'callback', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + params: [], + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + }, + }, + { + name: 'extras', + optional: true, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'ObjectTypeAnnotation', + properties: [ + { + optional: false, + name: 'key', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + optional: false, + name: 'value', + typeAnnotation: { + type: 'GenericObjectTypeAnnotation', + }, + }, + ], + }, + }, + }, + ], + }, + }, + }, + { + name: 'getArrays', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + params: [ + { + optional: false, + name: 'options', + typeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [ + { + optional: false, + name: 'arrayOfNumbers', + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'NumberTypeAnnotation', + }, + }, + }, + { + optional: true, + name: 'optionalArrayOfNumbers', + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'NumberTypeAnnotation', + }, + }, + }, + { + optional: false, + name: 'arrayOfStrings', + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'StringTypeAnnotation', + }, + }, + }, + { + optional: true, + name: 'optionalArrayOfStrings', + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'StringTypeAnnotation', + }, + }, + }, + { + optional: false, + name: 'arrayOfObjects', + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'ObjectTypeAnnotation', + properties: [ + { + optional: false, + name: 'numberProperty', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + ], + }, + }, + }, + ], + }, + }, + ], + }, + }, + { + name: 'getNullableObject', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'NullableTypeAnnotation', + typeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [], + }, + }, + params: [], + }, + }, + { + name: 'getNullableGenericObject', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'NullableTypeAnnotation', + typeAnnotation: { + type: 'GenericObjectTypeAnnotation', + }, + }, + params: [], + }, + }, + { + name: 'getNullableArray', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'NullableTypeAnnotation', + typeAnnotation: { + type: 'ArrayTypeAnnotation', + }, + }, + params: [], + }, + }, + ], + }, + moduleName: 'SampleTurboModule', + }, + }, +}; +const NATIVE_MODULES_WITH_TYPE_ALIASES = { + modules: { + AliasTurboModule: { + type: 'NativeModule', + aliasMap: { + Options: { + type: 'ObjectTypeAnnotation', + properties: [ + { + optional: false, + name: 'offset', + typeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [ + { + optional: false, + name: 'x', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + optional: false, + name: 'y', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + ], + }, + }, + { + optional: false, + name: 'size', + typeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [ + { + optional: false, + name: 'width', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + optional: false, + name: 'height', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + ], + }, + }, + { + optional: true, + name: 'displaySize', + typeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [ + { + optional: false, + name: 'width', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + optional: false, + name: 'height', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + ], + }, + }, + { + optional: true, + name: 'resizeMode', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + optional: true, + name: 'allowExternalStorage', + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + ], + }, + }, + enumMap: {}, + spec: { + eventEmitters: [], + methods: [ + { + name: 'getConstants', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [], + }, + params: [], + }, + }, + { + name: 'cropImage', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + params: [ + { + optional: false, + name: 'cropData', + typeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'Options', + }, + }, + ], + }, + }, + ], + }, + moduleName: 'AliasTurboModule', + }, + }, +}; +const REAL_MODULE_EXAMPLE = { + modules: { + NativeCameraRollManager: { + type: 'NativeModule', + aliasMap: { + PhotoIdentifierImage: { + type: 'ObjectTypeAnnotation', + properties: [ + { + optional: false, + name: 'uri', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + optional: false, + name: 'playableDuration', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + optional: false, + name: 'width', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + optional: false, + name: 'height', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + optional: true, + name: 'isStored', + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + { + optional: false, + name: 'filename', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + ], + }, + PhotoIdentifier: { + type: 'ObjectTypeAnnotation', + properties: [ + { + optional: false, + name: 'node', + typeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [ + { + optional: false, + name: 'image', + typeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'PhotoIdentifierImage', + }, + }, + { + optional: false, + name: 'type', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + optional: false, + name: 'group_name', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + optional: false, + name: 'timestamp', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + optional: false, + name: 'location', + typeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [ + { + optional: false, + name: 'longitude', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + optional: false, + name: 'latitude', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + optional: true, + name: 'altitude', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + optional: true, + name: 'heading', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + optional: true, + name: 'speed', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + PhotoIdentifiersPage: { + type: 'ObjectTypeAnnotation', + properties: [ + { + optional: false, + name: 'edges', + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'TypeAliasTypeAnnotation', + name: 'PhotoIdentifier', + }, + }, + }, + { + optional: false, + name: 'page_info', + typeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [ + { + optional: false, + name: 'has_next_page', + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + { + optional: true, + name: 'start_cursor', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + optional: true, + name: 'end_cursor', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + ], + }, + }, + ], + }, + GetPhotosParams: { + type: 'ObjectTypeAnnotation', + properties: [ + { + optional: false, + name: 'first', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + optional: true, + name: 'after', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + optional: true, + name: 'groupName', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + optional: true, + name: 'groupTypes', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + optional: true, + name: 'assetType', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + optional: true, + name: 'maxSize', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + optional: true, + name: 'mimeTypes', + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'StringTypeAnnotation', + }, + }, + }, + ], + }, + }, + enumMap: {}, + spec: { + eventEmitters: [], + methods: [ + { + name: 'getConstants', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [], + }, + params: [], + }, + }, + { + name: 'getPhotos', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'PromiseTypeAnnotation', + }, + params: [ + { + optional: false, + name: 'params', + typeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'GetPhotosParams', + }, + }, + ], + }, + }, + { + name: 'saveToCameraRoll', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'PromiseTypeAnnotation', + }, + params: [ + { + optional: false, + name: 'uri', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + optional: false, + name: 'type', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'deletePhotos', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'PromiseTypeAnnotation', + }, + params: [ + { + name: 'assets', + optional: false, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'StringTypeAnnotation', + }, + }, + }, + ], + }, + }, + ], + }, + moduleName: 'CameraRollManager', + }, + NativeExceptionsManager: { + type: 'NativeModule', + aliasMap: { + StackFrame: { + properties: [ + { + optional: true, + name: 'column', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + optional: false, + name: 'file', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + optional: true, + name: 'lineNumber', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + optional: false, + name: 'methodName', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + optional: true, + name: 'collapse', + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + ], + type: 'ObjectTypeAnnotation', + }, + ExceptionData: { + properties: [ + { + optional: false, + name: 'message', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + optional: false, + name: 'originalMessage', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + optional: false, + name: 'name', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + optional: false, + name: 'componentStack', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + optional: false, + name: 'stack', + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'TypeAliasTypeAnnotation', + name: 'StackFrame', + }, + }, + }, + { + optional: false, + name: 'id', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + optional: false, + name: 'isFatal', + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + { + optional: true, + name: 'extraData', + typeAnnotation: { + type: 'GenericObjectTypeAnnotation', + }, + }, + ], + type: 'ObjectTypeAnnotation', + }, + }, + enumMap: {}, + spec: { + eventEmitters: [], + methods: [ + { + name: 'reportFatalException', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + params: [ + { + optional: false, + name: 'message', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + name: 'stack', + optional: false, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'TypeAliasTypeAnnotation', + name: 'StackFrame', + }, + }, + }, + { + optional: false, + name: 'exceptionId', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'reportSoftException', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + params: [ + { + optional: false, + name: 'message', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + name: 'stack', + optional: false, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'TypeAliasTypeAnnotation', + name: 'StackFrame', + }, + }, + }, + { + optional: false, + name: 'exceptionId', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'reportException', + optional: true, + typeAnnotation: { + type: 'NullableTypeAnnotation', + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + params: [ + { + optional: false, + name: 'data', + typeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'ExceptionData', + }, + }, + ], + }, + }, + }, + { + name: 'updateExceptionMessage', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + params: [ + { + optional: false, + name: 'message', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + name: 'stack', + optional: false, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'TypeAliasTypeAnnotation', + name: 'StackFrame', + }, + }, + }, + { + optional: false, + name: 'exceptionId', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'dismissRedbox', + optional: true, + typeAnnotation: { + type: 'NullableTypeAnnotation', + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + params: [], + }, + }, + }, + ], + }, + moduleName: 'ExceptionsManager', + }, + }, +}; +const CXX_ONLY_NATIVE_MODULES = { + modules: { + NativeSampleTurboModule: { + type: 'NativeModule', + aliasMap: { + ConstantsStruct: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'const1', + optional: false, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + { + name: 'const2', + optional: false, + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + name: 'const3', + optional: false, + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + ], + }, + CustomHostObject: { + type: 'ObjectTypeAnnotation', + properties: [], + }, + BinaryTreeNode: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'left', + optional: true, + typeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'BinaryTreeNode', + }, + }, + { + name: 'value', + optional: false, + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + name: 'right', + optional: true, + typeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'BinaryTreeNode', + }, + }, + ], + }, + GraphNode: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'label', + optional: false, + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + name: 'neighbors', + optional: true, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'TypeAliasTypeAnnotation', + name: 'GraphNode', + }, + }, + }, + ], + }, + ObjectStruct: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'a', + optional: false, + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + name: 'b', + optional: false, + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + name: 'c', + optional: true, + typeAnnotation: { + type: 'NullableTypeAnnotation', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + }, + ], + }, + ValueStruct: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'x', + optional: false, + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + name: 'y', + optional: false, + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + name: 'z', + optional: false, + typeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'ObjectStruct', + }, + }, + ], + }, + MenuItem: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'label', + optional: false, + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + name: 'onPress', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + params: [ + { + name: 'value', + optional: false, + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + name: 'flag', + optional: false, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'shortcut', + optional: true, + typeAnnotation: { + type: 'NullableTypeAnnotation', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + }, + { + name: 'items', + optional: true, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'TypeAliasTypeAnnotation', + name: 'MenuItem', + }, + }, + }, + ], + }, + }, + enumMap: { + EnumInt: { + name: 'EnumInt', + type: 'EnumDeclarationWithMembers', + memberType: 'NumberTypeAnnotation', + members: [ + { + name: 'IA', + value: '23', + }, + { + name: 'IB', + value: '42', + }, + ], + }, + EnumFloat: { + name: 'EnumFloat', + type: 'EnumDeclarationWithMembers', + memberType: 'NumberTypeAnnotation', + members: [ + { + name: 'FA', + value: '1.23', + }, + { + name: 'FB', + value: '4.56', + }, + ], + }, + EnumNone: { + name: 'EnumNone', + type: 'EnumDeclarationWithMembers', + memberType: 'StringTypeAnnotation', + members: [ + { + name: 'NA', + value: 'NA', + }, + { + name: 'NB', + value: 'NB', + }, + ], + }, + EnumStr: { + name: 'EnumStr', + type: 'EnumDeclarationWithMembers', + memberType: 'StringTypeAnnotation', + members: [ + { + name: 'SA', + value: 's---a', + }, + { + name: 'SB', + value: 's---b', + }, + ], + }, + }, + spec: { + eventEmitters: [], + methods: [ + { + name: 'getArray', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'ArrayTypeAnnotation', + }, + params: [ + { + name: 'arg', + optional: false, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'getBool', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + params: [ + { + name: 'arg', + optional: false, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'getConstants', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'ConstantsStruct', + }, + params: [], + }, + }, + { + name: 'getCustomEnum', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + name: 'EnumInt', + type: 'EnumDeclaration', + memberType: 'NumberTypeAnnotation', + }, + params: [ + { + name: 'arg', + optional: false, + typeAnnotation: { + name: 'EnumInt', + type: 'EnumDeclaration', + memberType: 'NumberTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'getCustomHostObject', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'CustomHostObject', + }, + params: [], + }, + }, + { + name: 'consumeCustomHostObject', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'StringTypeAnnotation', + }, + params: [ + { + name: 'customHostObject', + optional: false, + typeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'CustomHostObject', + }, + }, + ], + }, + }, + { + name: 'getBinaryTreeNode', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'BinaryTreeNode', + }, + params: [ + { + name: 'arg', + optional: false, + typeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'BinaryTreeNode', + }, + }, + ], + }, + }, + { + name: 'getGraphNode', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'GraphNode', + }, + params: [ + { + name: 'arg', + optional: false, + typeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'GraphNode', + }, + }, + ], + }, + }, + { + name: 'getNumEnum', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + name: 'EnumFloat', + type: 'EnumDeclaration', + memberType: 'NumberTypeAnnotation', + }, + params: [ + { + name: 'arg', + optional: false, + typeAnnotation: { + name: 'EnumInt', + type: 'EnumDeclaration', + memberType: 'NumberTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'getStrEnum', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + name: 'EnumStr', + type: 'EnumDeclaration', + memberType: 'StringTypeAnnotation', + }, + params: [ + { + name: 'arg', + optional: false, + typeAnnotation: { + name: 'EnumNone', + type: 'EnumDeclaration', + memberType: 'StringTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'getMap', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'GenericObjectTypeAnnotation', + dictionaryValueType: { + type: 'NullableTypeAnnotation', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + }, + params: [ + { + name: 'arg', + optional: false, + typeAnnotation: { + type: 'GenericObjectTypeAnnotation', + dictionaryValueType: { + type: 'NullableTypeAnnotation', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + }, + }, + ], + }, + }, + { + name: 'getNumber', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'NumberTypeAnnotation', + }, + params: [ + { + name: 'arg', + optional: false, + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'getObject', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'ObjectStruct', + }, + params: [ + { + name: 'arg', + optional: false, + typeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'ObjectStruct', + }, + }, + ], + }, + }, + { + name: 'getSet', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'NumberTypeAnnotation', + }, + }, + params: [ + { + name: 'arg', + optional: false, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'NumberTypeAnnotation', + }, + }, + }, + ], + }, + }, + { + name: 'getString', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'StringTypeAnnotation', + }, + params: [ + { + name: 'arg', + optional: false, + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'getUnion', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'StringTypeAnnotation', + }, + params: [ + { + name: 'x', + optional: false, + typeAnnotation: { + type: 'UnionTypeAnnotation', + memberType: 'NumberTypeAnnotation', + }, + }, + { + name: 'y', + optional: false, + typeAnnotation: { + type: 'UnionTypeAnnotation', + memberType: 'StringTypeAnnotation', + }, + }, + { + name: 'z', + optional: false, + typeAnnotation: { + type: 'UnionTypeAnnotation', + memberType: 'ObjectTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'getValue', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'ValueStruct', + }, + params: [ + { + name: 'x', + optional: false, + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + name: 'y', + optional: false, + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + name: 'z', + optional: false, + typeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'ObjectStruct', + }, + }, + ], + }, + }, + { + name: 'getValueWithCallback', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + params: [ + { + name: 'callback', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + params: [ + { + name: 'value', + optional: false, + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + ], + }, + }, + ], + }, + }, + { + name: 'getValueWithPromise', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'PromiseTypeAnnotation', + elementType: { + type: 'StringTypeAnnotation', + }, + }, + params: [ + { + name: 'error', + optional: false, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'getWithWithOptionalArgs', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'NullableTypeAnnotation', + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + params: [ + { + name: 'optionalArg', + optional: true, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'voidFunc', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + params: [], + }, + }, + { + name: 'setMenu', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + params: [ + { + name: 'menuItem', + optional: false, + typeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'MenuItem', + }, + }, + ], + }, + }, + { + name: 'emitCustomDeviceEvent', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + params: [ + { + name: 'eventName', + optional: false, + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'voidFuncThrows', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + params: [], + }, + }, + { + name: 'getObjectThrows', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'ObjectStruct', + }, + params: [ + { + name: 'arg', + optional: false, + typeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'ObjectStruct', + }, + }, + ], + }, + }, + { + name: 'voidFuncAssert', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + params: [], + }, + }, + { + name: 'getObjectAssert', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'ObjectStruct', + }, + params: [ + { + name: 'arg', + optional: false, + typeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'ObjectStruct', + }, + }, + ], + }, + }, + ], + }, + moduleName: 'SampleTurboModuleCxx', + excludedPlatforms: ['iOS', 'android'], + }, + }, +}; +const SAMPLE_WITH_UPPERCASE_NAME = { + modules: { + NativeSampleTurboModule: { + type: 'NativeModule', + enumMap: {}, + aliasMap: {}, + spec: { + eventEmitters: [], + methods: [], + }, + moduleName: 'SampleTurboModule', + }, + }, +}; +module.exports = { + complex_objects: COMPLEX_OBJECTS, + two_modules_different_files: TWO_MODULES_DIFFERENT_FILES, + empty_native_modules: EMPTY_NATIVE_MODULES, + simple_native_modules: SIMPLE_NATIVE_MODULES, + native_modules_with_type_aliases: NATIVE_MODULES_WITH_TYPE_ALIASES, + real_module_example: REAL_MODULE_EXAMPLE, + cxx_only_native_modules: CXX_ONLY_NATIVE_MODULES, + SampleWithUppercaseName: SAMPLE_WITH_UPPERCASE_NAME, +}; diff --git a/packages/react-native-codegen/lib/generators/modules/__test_fixtures__/fixtures.js.flow b/packages/react-native-codegen/lib/generators/modules/__test_fixtures__/fixtures.js.flow new file mode 100644 index 00000000000000..aea2ffbcfbaccd --- /dev/null +++ b/packages/react-native-codegen/lib/generators/modules/__test_fixtures__/fixtures.js.flow @@ -0,0 +1,2443 @@ +/** + * 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-local + * @format + */ + +'use strict'; + +import type {SchemaType} from '../../../CodegenSchema.js'; + +const EMPTY_NATIVE_MODULES: SchemaType = { + modules: { + NativeSampleTurboModule: { + type: 'NativeModule', + aliasMap: {}, + enumMap: {}, + spec: { + eventEmitters: [], + methods: [], + }, + moduleName: 'SampleTurboModule', + }, + }, +}; + +const SIMPLE_NATIVE_MODULES: SchemaType = { + modules: { + NativeSampleTurboModule: { + type: 'NativeModule', + aliasMap: {}, + enumMap: { + NumEnum: { + type: 'EnumDeclarationWithMembers', + name: 'NumEnum', + memberType: 'NumberTypeAnnotation', + members: [ + { + name: 'ONE', + value: '1', + }, + { + name: 'TWO', + value: '2', + }, + ], + }, + FloatEnum: { + type: 'EnumDeclarationWithMembers', + name: 'FloatEnum', + memberType: 'NumberTypeAnnotation', + members: [ + { + name: 'POINT_ZERO', + value: '0.0', + }, + { + name: 'POINT_ONE', + value: '0.1', + }, + { + name: 'POINT_TWO', + value: '0.2', + }, + ], + }, + StringEnum: { + type: 'EnumDeclarationWithMembers', + name: 'StringEnum', + memberType: 'StringTypeAnnotation', + members: [ + { + name: 'HELLO', + value: 'hello', + }, + { + name: 'GoodBye', + value: 'goodbye', + }, + ], + }, + }, + spec: { + eventEmitters: [], + methods: [ + { + name: 'getConstants', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [ + { + optional: false, + name: 'const1', + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + { + optional: false, + name: 'const2', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + optional: false, + name: 'const3', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + ], + }, + params: [], + }, + }, + { + name: 'voidFunc', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + params: [], + }, + }, + { + name: 'getBool', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + params: [ + { + optional: false, + name: 'arg', + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'getNumber', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'NumberTypeAnnotation', + }, + params: [ + { + optional: false, + name: 'arg', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'getString', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'StringTypeAnnotation', + }, + params: [ + { + optional: false, + name: 'arg', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'getArray', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'GenericObjectTypeAnnotation', + }, + }, + params: [ + { + name: 'arg', + optional: false, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + + elementType: { + type: 'GenericObjectTypeAnnotation', + }, + }, + }, + ], + }, + }, + { + name: 'getObject', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'GenericObjectTypeAnnotation', + }, + params: [ + { + optional: false, + name: 'arg', + typeAnnotation: { + type: 'GenericObjectTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'getRootTag', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'ReservedTypeAnnotation', + name: 'RootTag', + }, + params: [ + { + optional: false, + name: 'arg', + typeAnnotation: { + type: 'ReservedTypeAnnotation', + name: 'RootTag', + }, + }, + ], + }, + }, + { + name: 'getValue', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'GenericObjectTypeAnnotation', + }, + params: [ + { + optional: false, + name: 'x', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + optional: false, + name: 'y', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + optional: false, + name: 'z', + typeAnnotation: { + type: 'GenericObjectTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'getEnumReturn', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'EnumDeclaration', + name: 'NumEnum', + memberType: 'NumberTypeAnnotation', + }, + params: [], + }, + }, + { + name: 'getValueWithCallback', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + params: [ + { + name: 'callback', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + params: [], + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + }, + }, + ], + }, + }, + { + name: 'getValueWithPromise', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'PromiseTypeAnnotation', + }, + params: [ + { + optional: false, + name: 'error', + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'getValueWithOptionalArg', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'PromiseTypeAnnotation', + }, + params: [ + { + optional: true, + name: 'parameter', + typeAnnotation: { + type: 'GenericObjectTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'getEnums', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'StringTypeAnnotation', + }, + params: [ + { + name: 'enumInt', + optional: false, + typeAnnotation: { + name: 'NumEnum', + type: 'EnumDeclaration', + memberType: 'NumberTypeAnnotation', + }, + }, + { + name: 'enumFloat', + optional: false, + typeAnnotation: { + name: 'FloatEnum', + type: 'EnumDeclaration', + memberType: 'NumberTypeAnnotation', + }, + }, + { + name: 'enumString', + optional: false, + typeAnnotation: { + name: 'StringEnum', + type: 'EnumDeclaration', + memberType: 'StringTypeAnnotation', + }, + }, + ], + }, + }, + ], + }, + moduleName: 'SampleTurboModule', + }, + }, +}; + +const TWO_MODULES_DIFFERENT_FILES: SchemaType = { + modules: { + NativeSampleTurboModule: { + type: 'NativeModule', + aliasMap: {}, + enumMap: {}, + spec: { + eventEmitters: [], + methods: [ + { + name: 'voidFunc', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + params: [], + }, + }, + ], + }, + moduleName: 'SampleTurboModule', + }, + NativeSampleTurboModule2: { + type: 'NativeModule', + aliasMap: {}, + enumMap: {}, + spec: { + eventEmitters: [], + methods: [ + { + name: 'getConstants', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [], + }, + params: [], + }, + }, + { + name: 'voidFunc', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + params: [], + }, + }, + ], + }, + moduleName: 'SampleTurboModule2', + }, + }, +}; + +const COMPLEX_OBJECTS: SchemaType = { + modules: { + NativeSampleTurboModule: { + type: 'NativeModule', + aliasMap: {}, + enumMap: {}, + spec: { + eventEmitters: [], + methods: [ + { + name: 'difficult', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [ + { + optional: false, + name: 'D', + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + { + optional: false, + name: 'E', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + optional: false, + name: 'F', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + ], + }, + params: [ + { + optional: false, + name: 'A', + typeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [ + { + optional: false, + name: 'D', + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + { + optional: false, + name: 'E', + typeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [ + { + optional: false, + name: 'D', + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + { + optional: false, + name: 'E', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + optional: false, + name: 'F', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + optional: false, + name: 'id', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + ], + }, + }, + { + optional: false, + name: 'F', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + ], + }, + }, + ], + }, + }, + { + name: 'optionals', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + params: [ + { + optional: false, + name: 'A', + typeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [ + { + optional: true, + name: 'optionalNumberProperty', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + optional: true, + name: 'optionalArrayProperty', + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'NumberTypeAnnotation', + }, + }, + }, + { + optional: true, + name: 'optionalObjectProperty', + typeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [ + { + optional: false, + name: 'x', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + optional: false, + name: 'y', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + ], + }, + }, + { + optional: true, + name: 'optionalGenericObjectProperty', + typeAnnotation: { + type: 'GenericObjectTypeAnnotation', + }, + }, + { + optional: true, + name: 'optionalBooleanTypeProperty', + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + ], + }, + }, + ], + }, + }, + { + name: 'optionalMethod', + optional: true, + typeAnnotation: { + type: 'NullableTypeAnnotation', + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + params: [ + { + optional: false, + name: 'options', + typeAnnotation: { + type: 'GenericObjectTypeAnnotation', + }, + }, + { + name: 'callback', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + params: [], + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + }, + }, + { + name: 'extras', + optional: true, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'ObjectTypeAnnotation', + properties: [ + { + optional: false, + name: 'key', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + optional: false, + name: 'value', + typeAnnotation: { + type: 'GenericObjectTypeAnnotation', + }, + }, + ], + }, + }, + }, + ], + }, + }, + }, + { + name: 'getArrays', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + params: [ + { + optional: false, + name: 'options', + typeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [ + { + optional: false, + name: 'arrayOfNumbers', + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'NumberTypeAnnotation', + }, + }, + }, + { + optional: true, + name: 'optionalArrayOfNumbers', + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'NumberTypeAnnotation', + }, + }, + }, + { + optional: false, + name: 'arrayOfStrings', + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'StringTypeAnnotation', + }, + }, + }, + { + optional: true, + name: 'optionalArrayOfStrings', + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'StringTypeAnnotation', + }, + }, + }, + { + optional: false, + name: 'arrayOfObjects', + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'ObjectTypeAnnotation', + properties: [ + { + optional: false, + name: 'numberProperty', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + ], + }, + }, + }, + ], + }, + }, + ], + }, + }, + { + name: 'getNullableObject', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'NullableTypeAnnotation', + typeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [], + }, + }, + params: [], + }, + }, + { + name: 'getNullableGenericObject', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'NullableTypeAnnotation', + typeAnnotation: { + type: 'GenericObjectTypeAnnotation', + }, + }, + params: [], + }, + }, + { + name: 'getNullableArray', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'NullableTypeAnnotation', + typeAnnotation: { + type: 'ArrayTypeAnnotation', + }, + }, + params: [], + }, + }, + ], + }, + moduleName: 'SampleTurboModule', + }, + }, +}; + +const NATIVE_MODULES_WITH_TYPE_ALIASES: SchemaType = { + modules: { + AliasTurboModule: { + type: 'NativeModule', + aliasMap: { + Options: { + type: 'ObjectTypeAnnotation', + properties: [ + { + optional: false, + name: 'offset', + typeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [ + { + optional: false, + name: 'x', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + optional: false, + name: 'y', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + ], + }, + }, + { + optional: false, + name: 'size', + typeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [ + { + optional: false, + name: 'width', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + optional: false, + name: 'height', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + ], + }, + }, + { + optional: true, + name: 'displaySize', + typeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [ + { + optional: false, + name: 'width', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + optional: false, + name: 'height', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + ], + }, + }, + { + optional: true, + name: 'resizeMode', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + optional: true, + name: 'allowExternalStorage', + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + ], + }, + }, + enumMap: {}, + spec: { + eventEmitters: [], + methods: [ + { + name: 'getConstants', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [], + }, + params: [], + }, + }, + { + name: 'cropImage', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + params: [ + { + optional: false, + name: 'cropData', + typeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'Options', + }, + }, + ], + }, + }, + ], + }, + moduleName: 'AliasTurboModule', + }, + }, +}; + +const REAL_MODULE_EXAMPLE: SchemaType = { + modules: { + NativeCameraRollManager: { + type: 'NativeModule', + aliasMap: { + PhotoIdentifierImage: { + type: 'ObjectTypeAnnotation', + properties: [ + { + optional: false, + name: 'uri', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + optional: false, + name: 'playableDuration', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + optional: false, + name: 'width', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + optional: false, + name: 'height', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + optional: true, + name: 'isStored', + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + { + optional: false, + name: 'filename', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + ], + }, + PhotoIdentifier: { + type: 'ObjectTypeAnnotation', + properties: [ + { + optional: false, + name: 'node', + typeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [ + { + optional: false, + name: 'image', + typeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'PhotoIdentifierImage', + }, + }, + { + optional: false, + name: 'type', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + optional: false, + name: 'group_name', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + optional: false, + name: 'timestamp', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + optional: false, + name: 'location', + typeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [ + { + optional: false, + name: 'longitude', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + optional: false, + name: 'latitude', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + optional: true, + name: 'altitude', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + optional: true, + name: 'heading', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + optional: true, + name: 'speed', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + PhotoIdentifiersPage: { + type: 'ObjectTypeAnnotation', + properties: [ + { + optional: false, + name: 'edges', + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'TypeAliasTypeAnnotation', + name: 'PhotoIdentifier', + }, + }, + }, + { + optional: false, + name: 'page_info', + typeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [ + { + optional: false, + name: 'has_next_page', + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + { + optional: true, + name: 'start_cursor', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + optional: true, + name: 'end_cursor', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + ], + }, + }, + ], + }, + GetPhotosParams: { + type: 'ObjectTypeAnnotation', + properties: [ + { + optional: false, + name: 'first', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + optional: true, + name: 'after', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + optional: true, + name: 'groupName', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + optional: true, + name: 'groupTypes', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + optional: true, + name: 'assetType', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + optional: true, + name: 'maxSize', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + optional: true, + name: 'mimeTypes', + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'StringTypeAnnotation', + }, + }, + }, + ], + }, + }, + enumMap: {}, + spec: { + eventEmitters: [], + methods: [ + { + name: 'getConstants', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'ObjectTypeAnnotation', + properties: [], + }, + params: [], + }, + }, + { + name: 'getPhotos', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'PromiseTypeAnnotation', + }, + params: [ + { + optional: false, + name: 'params', + typeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'GetPhotosParams', + }, + }, + ], + }, + }, + { + name: 'saveToCameraRoll', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'PromiseTypeAnnotation', + }, + params: [ + { + optional: false, + name: 'uri', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + optional: false, + name: 'type', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'deletePhotos', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'PromiseTypeAnnotation', + }, + params: [ + { + name: 'assets', + optional: false, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'StringTypeAnnotation', + }, + }, + }, + ], + }, + }, + ], + }, + moduleName: 'CameraRollManager', + }, + NativeExceptionsManager: { + type: 'NativeModule', + aliasMap: { + StackFrame: { + properties: [ + { + optional: true, + name: 'column', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + optional: false, + name: 'file', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + optional: true, + name: 'lineNumber', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + optional: false, + name: 'methodName', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + optional: true, + name: 'collapse', + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + ], + type: 'ObjectTypeAnnotation', + }, + ExceptionData: { + properties: [ + { + optional: false, + name: 'message', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + optional: false, + name: 'originalMessage', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + optional: false, + name: 'name', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + optional: false, + name: 'componentStack', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + optional: false, + name: 'stack', + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'TypeAliasTypeAnnotation', + name: 'StackFrame', + }, + }, + }, + { + optional: false, + name: 'id', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + optional: false, + name: 'isFatal', + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + { + optional: true, + name: 'extraData', + typeAnnotation: { + type: 'GenericObjectTypeAnnotation', + }, + }, + ], + type: 'ObjectTypeAnnotation', + }, + }, + enumMap: {}, + spec: { + eventEmitters: [], + methods: [ + { + name: 'reportFatalException', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + params: [ + { + optional: false, + name: 'message', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + name: 'stack', + optional: false, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'TypeAliasTypeAnnotation', + name: 'StackFrame', + }, + }, + }, + { + optional: false, + name: 'exceptionId', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'reportSoftException', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + params: [ + { + optional: false, + name: 'message', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + name: 'stack', + optional: false, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'TypeAliasTypeAnnotation', + name: 'StackFrame', + }, + }, + }, + { + optional: false, + name: 'exceptionId', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'reportException', + optional: true, + typeAnnotation: { + type: 'NullableTypeAnnotation', + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + params: [ + { + optional: false, + name: 'data', + typeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'ExceptionData', + }, + }, + ], + }, + }, + }, + { + name: 'updateExceptionMessage', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + params: [ + { + optional: false, + name: 'message', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + name: 'stack', + optional: false, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'TypeAliasTypeAnnotation', + name: 'StackFrame', + }, + }, + }, + { + optional: false, + name: 'exceptionId', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'dismissRedbox', + optional: true, + typeAnnotation: { + type: 'NullableTypeAnnotation', + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + params: [], + }, + }, + }, + ], + }, + moduleName: 'ExceptionsManager', + }, + }, +}; + +const CXX_ONLY_NATIVE_MODULES: SchemaType = { + modules: { + NativeSampleTurboModule: { + type: 'NativeModule', + aliasMap: { + ConstantsStruct: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'const1', + optional: false, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + { + name: 'const2', + optional: false, + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + name: 'const3', + optional: false, + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + ], + }, + CustomHostObject: { + type: 'ObjectTypeAnnotation', + properties: [], + }, + BinaryTreeNode: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'left', + optional: true, + typeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'BinaryTreeNode', + }, + }, + { + name: 'value', + optional: false, + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + name: 'right', + optional: true, + typeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'BinaryTreeNode', + }, + }, + ], + }, + GraphNode: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'label', + optional: false, + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + name: 'neighbors', + optional: true, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'TypeAliasTypeAnnotation', + name: 'GraphNode', + }, + }, + }, + ], + }, + ObjectStruct: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'a', + optional: false, + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + name: 'b', + optional: false, + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + name: 'c', + optional: true, + typeAnnotation: { + type: 'NullableTypeAnnotation', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + }, + ], + }, + ValueStruct: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'x', + optional: false, + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + name: 'y', + optional: false, + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + name: 'z', + optional: false, + typeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'ObjectStruct', + }, + }, + ], + }, + MenuItem: { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'label', + optional: false, + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + name: 'onPress', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + params: [ + { + name: 'value', + optional: false, + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + name: 'flag', + optional: false, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'shortcut', + optional: true, + typeAnnotation: { + type: 'NullableTypeAnnotation', + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + }, + { + name: 'items', + optional: true, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'TypeAliasTypeAnnotation', + name: 'MenuItem', + }, + }, + }, + ], + }, + }, + enumMap: { + EnumInt: { + name: 'EnumInt', + type: 'EnumDeclarationWithMembers', + memberType: 'NumberTypeAnnotation', + members: [ + { + name: 'IA', + value: '23', + }, + { + name: 'IB', + value: '42', + }, + ], + }, + EnumFloat: { + name: 'EnumFloat', + type: 'EnumDeclarationWithMembers', + memberType: 'NumberTypeAnnotation', + members: [ + { + name: 'FA', + value: '1.23', + }, + { + name: 'FB', + value: '4.56', + }, + ], + }, + EnumNone: { + name: 'EnumNone', + type: 'EnumDeclarationWithMembers', + memberType: 'StringTypeAnnotation', + members: [ + { + name: 'NA', + value: 'NA', + }, + { + name: 'NB', + value: 'NB', + }, + ], + }, + EnumStr: { + name: 'EnumStr', + type: 'EnumDeclarationWithMembers', + memberType: 'StringTypeAnnotation', + members: [ + { + name: 'SA', + value: 's---a', + }, + { + name: 'SB', + value: 's---b', + }, + ], + }, + }, + spec: { + eventEmitters: [], + methods: [ + { + name: 'getArray', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'ArrayTypeAnnotation', + }, + params: [ + { + name: 'arg', + optional: false, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'getBool', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + params: [ + { + name: 'arg', + optional: false, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'getConstants', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'ConstantsStruct', + }, + params: [], + }, + }, + { + name: 'getCustomEnum', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + name: 'EnumInt', + type: 'EnumDeclaration', + memberType: 'NumberTypeAnnotation', + }, + params: [ + { + name: 'arg', + optional: false, + typeAnnotation: { + name: 'EnumInt', + type: 'EnumDeclaration', + memberType: 'NumberTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'getCustomHostObject', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'CustomHostObject', + }, + params: [], + }, + }, + { + name: 'consumeCustomHostObject', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'StringTypeAnnotation', + }, + params: [ + { + name: 'customHostObject', + optional: false, + typeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'CustomHostObject', + }, + }, + ], + }, + }, + { + name: 'getBinaryTreeNode', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'BinaryTreeNode', + }, + params: [ + { + name: 'arg', + optional: false, + typeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'BinaryTreeNode', + }, + }, + ], + }, + }, + { + name: 'getGraphNode', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'GraphNode', + }, + params: [ + { + name: 'arg', + optional: false, + typeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'GraphNode', + }, + }, + ], + }, + }, + { + name: 'getNumEnum', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + name: 'EnumFloat', + type: 'EnumDeclaration', + memberType: 'NumberTypeAnnotation', + }, + params: [ + { + name: 'arg', + optional: false, + typeAnnotation: { + name: 'EnumInt', + type: 'EnumDeclaration', + memberType: 'NumberTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'getStrEnum', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + name: 'EnumStr', + type: 'EnumDeclaration', + memberType: 'StringTypeAnnotation', + }, + params: [ + { + name: 'arg', + optional: false, + typeAnnotation: { + name: 'EnumNone', + type: 'EnumDeclaration', + memberType: 'StringTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'getMap', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'GenericObjectTypeAnnotation', + dictionaryValueType: { + type: 'NullableTypeAnnotation', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + }, + params: [ + { + name: 'arg', + optional: false, + typeAnnotation: { + type: 'GenericObjectTypeAnnotation', + dictionaryValueType: { + type: 'NullableTypeAnnotation', + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + }, + }, + ], + }, + }, + { + name: 'getNumber', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'NumberTypeAnnotation', + }, + params: [ + { + name: 'arg', + optional: false, + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'getObject', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'ObjectStruct', + }, + params: [ + { + name: 'arg', + optional: false, + typeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'ObjectStruct', + }, + }, + ], + }, + }, + { + name: 'getSet', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'NumberTypeAnnotation', + }, + }, + params: [ + { + name: 'arg', + optional: false, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'NumberTypeAnnotation', + }, + }, + }, + ], + }, + }, + { + name: 'getString', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'StringTypeAnnotation', + }, + params: [ + { + name: 'arg', + optional: false, + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'getUnion', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'StringTypeAnnotation', + }, + params: [ + { + name: 'x', + optional: false, + typeAnnotation: { + type: 'UnionTypeAnnotation', + memberType: 'NumberTypeAnnotation', + }, + }, + { + name: 'y', + optional: false, + typeAnnotation: { + type: 'UnionTypeAnnotation', + memberType: 'StringTypeAnnotation', + }, + }, + { + name: 'z', + optional: false, + typeAnnotation: { + type: 'UnionTypeAnnotation', + memberType: 'ObjectTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'getValue', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'ValueStruct', + }, + params: [ + { + name: 'x', + optional: false, + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + name: 'y', + optional: false, + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + name: 'z', + optional: false, + typeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'ObjectStruct', + }, + }, + ], + }, + }, + { + name: 'getValueWithCallback', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + params: [ + { + name: 'callback', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + params: [ + { + name: 'value', + optional: false, + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + ], + }, + }, + ], + }, + }, + { + name: 'getValueWithPromise', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'PromiseTypeAnnotation', + elementType: { + type: 'StringTypeAnnotation', + }, + }, + params: [ + { + name: 'error', + optional: false, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'getWithWithOptionalArgs', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'NullableTypeAnnotation', + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + params: [ + { + name: 'optionalArg', + optional: true, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'voidFunc', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + params: [], + }, + }, + { + name: 'setMenu', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + params: [ + { + name: 'menuItem', + optional: false, + typeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'MenuItem', + }, + }, + ], + }, + }, + { + name: 'emitCustomDeviceEvent', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + params: [ + { + name: 'eventName', + optional: false, + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + ], + }, + }, + { + name: 'voidFuncThrows', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + params: [], + }, + }, + + { + name: 'getObjectThrows', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'ObjectStruct', + }, + params: [ + { + name: 'arg', + optional: false, + typeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'ObjectStruct', + }, + }, + ], + }, + }, + { + name: 'voidFuncAssert', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + params: [], + }, + }, + { + name: 'getObjectAssert', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'ObjectStruct', + }, + params: [ + { + name: 'arg', + optional: false, + typeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'ObjectStruct', + }, + }, + ], + }, + }, + ], + }, + moduleName: 'SampleTurboModuleCxx', + excludedPlatforms: ['iOS', 'android'], + }, + }, +}; + +const SAMPLE_WITH_UPPERCASE_NAME: SchemaType = { + modules: { + NativeSampleTurboModule: { + type: 'NativeModule', + enumMap: {}, + aliasMap: {}, + spec: { + eventEmitters: [], + methods: [], + }, + moduleName: 'SampleTurboModule', + }, + }, +}; + +module.exports = { + complex_objects: COMPLEX_OBJECTS, + two_modules_different_files: TWO_MODULES_DIFFERENT_FILES, + empty_native_modules: EMPTY_NATIVE_MODULES, + simple_native_modules: SIMPLE_NATIVE_MODULES, + native_modules_with_type_aliases: NATIVE_MODULES_WITH_TYPE_ALIASES, + real_module_example: REAL_MODULE_EXAMPLE, + cxx_only_native_modules: CXX_ONLY_NATIVE_MODULES, + SampleWithUppercaseName: SAMPLE_WITH_UPPERCASE_NAME, +}; diff --git a/packages/react-native-codegen/lib/parsers/consistency/compareSnaps.js b/packages/react-native-codegen/lib/parsers/consistency/compareSnaps.js new file mode 100644 index 00000000000000..0e549003cae865 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/consistency/compareSnaps.js @@ -0,0 +1,84 @@ +/** + * 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 + * @oncall react_native + */ + +'use strict'; + +function compareSnaps( + flowFixtures, + flowSnaps, + flowExtraCases, + tsFixtures, + tsSnaps, + tsExtraCases, + ignoredCases, +) { + const flowCases = Object.keys(flowFixtures).sort(); + const tsCases = Object.keys(tsFixtures).sort(); + const commonCases = flowCases.filter(name => tsCases.indexOf(name) !== -1); + describe('RN Codegen Parsers', () => { + it('should not unintentionally contains test case for Flow but not for TypeScript', () => { + expect( + flowCases.filter(name => commonCases.indexOf(name) === -1), + ).toEqual(flowExtraCases); + }); + it('should not unintentionally contains test case for TypeScript but not for Flow', () => { + expect(tsCases.filter(name => commonCases.indexOf(name) === -1)).toEqual( + tsExtraCases, + ); + }); + for (const commonCase of commonCases) { + const flowSnap = + flowSnaps[ + `RN Codegen Flow Parser can generate fixture ${commonCase} 1` + ]; + const tsSnap = + tsSnaps[ + `RN Codegen TypeScript Parser can generate fixture ${commonCase} 1` + ]; + it(`should be able to find the snapshot for Flow for case ${commonCase}`, () => { + expect(typeof flowSnap).toEqual('string'); + }); + it(`should be able to find the snapshot for TypeScript for case ${commonCase}`, () => { + expect(typeof tsSnap).toEqual('string'); + }); + if (ignoredCases.indexOf(commonCase) === -1) { + it(`should generate the same snapshot from Flow and TypeScript for fixture ${commonCase}`, () => { + expect(flowSnap).toEqual(tsSnap); + }); + } else { + it(`should generate the different snapshot from Flow and TypeScript for fixture ${commonCase}`, () => { + expect(flowSnap).not.toEqual(tsSnap); + }); + } + } + }); +} +function compareTsArraySnaps(tsSnaps, tsExtraCases) { + for (const array2Case of tsExtraCases.filter( + name => name.indexOf('ARRAY2') !== -1, + )) { + const arrayCase = array2Case.replace('ARRAY2', 'ARRAY'); + it(`should generate the same snap from fixture ${arrayCase} and ${array2Case}`, () => { + expect( + tsSnaps[ + `RN Codegen TypeScript Parser can generate fixture ${arrayCase}` + ], + ).toEqual( + tsSnaps[ + `RN Codegen TypeScript Parser can generate fixture ${array2Case}` + ], + ); + }); + } +} +module.exports = { + compareSnaps, + compareTsArraySnaps, +}; diff --git a/packages/react-native-codegen/lib/parsers/error-utils.js b/packages/react-native-codegen/lib/parsers/error-utils.js new file mode 100644 index 00000000000000..d0b04711c60073 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/error-utils.js @@ -0,0 +1,391 @@ +/** + * 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 _require = require('./errors'), + IncorrectModuleRegistryCallArgumentTypeParserError = + _require.IncorrectModuleRegistryCallArgumentTypeParserError, + IncorrectModuleRegistryCallArityParserError = + _require.IncorrectModuleRegistryCallArityParserError, + IncorrectModuleRegistryCallTypeParameterParserError = + _require.IncorrectModuleRegistryCallTypeParameterParserError, + MisnamedModuleInterfaceParserError = + _require.MisnamedModuleInterfaceParserError, + ModuleInterfaceNotFoundParserError = + _require.ModuleInterfaceNotFoundParserError, + MoreThanOneModuleInterfaceParserError = + _require.MoreThanOneModuleInterfaceParserError, + MoreThanOneModuleRegistryCallsParserError = + _require.MoreThanOneModuleRegistryCallsParserError, + UnsupportedArrayElementTypeAnnotationParserError = + _require.UnsupportedArrayElementTypeAnnotationParserError, + UnsupportedFunctionParamTypeAnnotationParserError = + _require.UnsupportedFunctionParamTypeAnnotationParserError, + UnsupportedFunctionReturnTypeAnnotationParserError = + _require.UnsupportedFunctionReturnTypeAnnotationParserError, + UnsupportedModuleEventEmitterPropertyParserError = + _require.UnsupportedModuleEventEmitterPropertyParserError, + UnsupportedModuleEventEmitterTypePropertyParserError = + _require.UnsupportedModuleEventEmitterTypePropertyParserError, + UnsupportedModulePropertyParserError = + _require.UnsupportedModulePropertyParserError, + UnsupportedObjectPropertyValueTypeAnnotationParserError = + _require.UnsupportedObjectPropertyValueTypeAnnotationParserError, + UntypedModuleRegistryCallParserError = + _require.UntypedModuleRegistryCallParserError, + UnusedModuleInterfaceParserError = _require.UnusedModuleInterfaceParserError; +function throwIfModuleInterfaceIsMisnamed( + nativeModuleName, + moduleSpecId, + parserType, +) { + if (moduleSpecId.name !== 'Spec') { + throw new MisnamedModuleInterfaceParserError( + nativeModuleName, + moduleSpecId, + parserType, + ); + } +} +function throwIfModuleInterfaceNotFound( + numberOfModuleSpecs, + nativeModuleName, + ast, + parserType, +) { + if (numberOfModuleSpecs === 0) { + throw new ModuleInterfaceNotFoundParserError( + nativeModuleName, + ast, + parserType, + ); + } +} +function throwIfMoreThanOneModuleRegistryCalls( + hasteModuleName, + callExpressions, + callExpressionsLength, +) { + if (callExpressions.length > 1) { + throw new MoreThanOneModuleRegistryCallsParserError( + hasteModuleName, + callExpressions, + callExpressionsLength, + ); + } +} +function throwIfUnusedModuleInterfaceParserError( + nativeModuleName, + moduleSpec, + callExpressions, +) { + if (callExpressions.length === 0) { + throw new UnusedModuleInterfaceParserError(nativeModuleName, moduleSpec); + } +} +function throwIfWrongNumberOfCallExpressionArgs( + nativeModuleName, + flowCallExpression, + methodName, + numberOfCallExpressionArgs, +) { + if (numberOfCallExpressionArgs !== 1) { + throw new IncorrectModuleRegistryCallArityParserError( + nativeModuleName, + flowCallExpression, + methodName, + numberOfCallExpressionArgs, + ); + } +} +function throwIfIncorrectModuleRegistryCallTypeParameterParserError( + nativeModuleName, + typeArguments, + methodName, + moduleName, + parser, +) { + function throwError() { + throw new IncorrectModuleRegistryCallTypeParameterParserError( + nativeModuleName, + typeArguments, + methodName, + moduleName, + ); + } + if (parser.checkIfInvalidModule(typeArguments)) { + throwError(); + } +} +function throwIfUnsupportedFunctionReturnTypeAnnotationParserError( + nativeModuleName, + returnTypeAnnotation, + invalidReturnType, + cxxOnly, + returnType, +) { + if (!cxxOnly && returnType === 'FunctionTypeAnnotation') { + throw new UnsupportedFunctionReturnTypeAnnotationParserError( + nativeModuleName, + returnTypeAnnotation.returnType, + 'FunctionTypeAnnotation', + ); + } +} +function throwIfUntypedModule( + typeArguments, + hasteModuleName, + callExpression, + methodName, + moduleName, +) { + if (typeArguments == null) { + throw new UntypedModuleRegistryCallParserError( + hasteModuleName, + callExpression, + methodName, + moduleName, + ); + } +} +function throwIfEventEmitterTypeIsUnsupported( + nativeModuleName, + propertyName, + propertyValueType, + parser, + nullable, + untyped, + cxxOnly, +) { + if (nullable || untyped || !cxxOnly) { + throw new UnsupportedModuleEventEmitterPropertyParserError( + nativeModuleName, + propertyName, + propertyValueType, + parser.language(), + nullable, + untyped, + cxxOnly, + ); + } +} +function throwIfEventEmitterEventTypeIsUnsupported( + nativeModuleName, + propertyName, + propertyValueType, + parser, + nullable, +) { + if (nullable) { + throw new UnsupportedModuleEventEmitterTypePropertyParserError( + nativeModuleName, + propertyName, + propertyValueType, + parser.language(), + nullable, + ); + } +} +function throwIfModuleTypeIsUnsupported( + nativeModuleName, + propertyValue, + propertyName, + propertyValueType, + parser, +) { + if (!parser.functionTypeAnnotation(propertyValueType)) { + throw new UnsupportedModulePropertyParserError( + nativeModuleName, + propertyValue, + propertyName, + propertyValueType, + parser.language(), + ); + } +} +const UnsupportedObjectPropertyTypeToInvalidPropertyValueTypeMap = { + FunctionTypeAnnotation: 'FunctionTypeAnnotation', + VoidTypeAnnotation: 'void', + PromiseTypeAnnotation: 'Promise', +}; +function throwIfPropertyValueTypeIsUnsupported( + moduleName, + propertyValue, + propertyKey, + type, +) { + const invalidPropertyValueType = + // $FlowFixMe[invalid-computed-prop] + UnsupportedObjectPropertyTypeToInvalidPropertyValueTypeMap[type]; + throw new UnsupportedObjectPropertyValueTypeAnnotationParserError( + moduleName, + propertyValue, + propertyKey, + invalidPropertyValueType, + ); +} +function throwIfMoreThanOneModuleInterfaceParserError( + nativeModuleName, + moduleSpecs, + parserType, +) { + if (moduleSpecs.length > 1) { + throw new MoreThanOneModuleInterfaceParserError( + nativeModuleName, + moduleSpecs, + moduleSpecs.map(node => node.id.name), + parserType, + ); + } +} +function throwIfUnsupportedFunctionParamTypeAnnotationParserError( + nativeModuleName, + languageParamTypeAnnotation, + paramName, + paramTypeAnnotationType, +) { + throw new UnsupportedFunctionParamTypeAnnotationParserError( + nativeModuleName, + languageParamTypeAnnotation, + paramName, + paramTypeAnnotationType, + ); +} +function throwIfArrayElementTypeAnnotationIsUnsupported( + hasteModuleName, + flowElementType, + flowArrayType, + type, +) { + const TypeMap = { + FunctionTypeAnnotation: 'FunctionTypeAnnotation', + VoidTypeAnnotation: 'void', + PromiseTypeAnnotation: 'Promise', + // TODO: Added as a work-around for now until TupleTypeAnnotation are fully supported in both flow and TS + // Right now they are partially treated as UnionTypeAnnotation + UnionTypeAnnotation: 'UnionTypeAnnotation', + }; + if (type in TypeMap) { + throw new UnsupportedArrayElementTypeAnnotationParserError( + hasteModuleName, + flowElementType, + flowArrayType, + // $FlowFixMe[invalid-computed-prop] + TypeMap[type], + ); + } +} +function throwIfIncorrectModuleRegistryCallArgument( + nativeModuleName, + callExpressionArg, + methodName, +) { + if ( + callExpressionArg.type !== 'StringLiteral' && + callExpressionArg.type !== 'Literal' + ) { + const type = callExpressionArg.type; + throw new IncorrectModuleRegistryCallArgumentTypeParserError( + nativeModuleName, + callExpressionArg, + methodName, + type, + ); + } +} +function throwIfPartialNotAnnotatingTypeParameter( + typeAnnotation, + types, + parser, +) { + const annotatedElement = parser.extractAnnotatedElement( + typeAnnotation, + types, + ); + if (!annotatedElement) { + throw new Error('Partials only support annotating a type parameter.'); + } +} +function throwIfPartialWithMoreParameter(typeAnnotation) { + if (typeAnnotation.typeParameters.params.length !== 1) { + throw new Error('Partials only support annotating exactly one parameter.'); + } +} +function throwIfMoreThanOneCodegenNativecommands(commandsTypeNames) { + if (commandsTypeNames.length > 1) { + throw new Error('codegenNativeCommands may only be called once in a file'); + } +} +function throwIfConfigNotfound(foundConfigs) { + if (foundConfigs.length === 0) { + throw new Error('Could not find component config for native component'); + } +} +function throwIfMoreThanOneConfig(foundConfigs) { + if (foundConfigs.length > 1) { + throw new Error('Only one component is supported per file'); + } +} +function throwIfEventHasNoName(typeAnnotation, parser) { + const name = + parser.language() === 'Flow' ? typeAnnotation.id : typeAnnotation.typeName; + if (!name) { + throw new Error("typeAnnotation of event doesn't have a name"); + } +} +function throwIfBubblingTypeIsNull(bubblingType, eventName) { + if (!bubblingType) { + throw new Error( + `Unable to determine event bubbling type for "${eventName}"`, + ); + } + return bubblingType; +} +function throwIfArgumentPropsAreNull(argumentProps, eventName) { + if (!argumentProps) { + throw new Error(`Unable to determine event arguments for "${eventName}"`); + } + return argumentProps; +} +function throwIfTypeAliasIsNotInterface(typeAlias, parser) { + if (typeAlias.type !== parser.interfaceDeclaration) { + throw new Error( + `The type argument for codegenNativeCommands must be an interface, received ${typeAlias.type}`, + ); + } +} +module.exports = { + throwIfModuleInterfaceIsMisnamed, + throwIfUnsupportedFunctionReturnTypeAnnotationParserError, + throwIfModuleInterfaceNotFound, + throwIfMoreThanOneModuleRegistryCalls, + throwIfPropertyValueTypeIsUnsupported, + throwIfUnusedModuleInterfaceParserError, + throwIfWrongNumberOfCallExpressionArgs, + throwIfIncorrectModuleRegistryCallTypeParameterParserError, + throwIfUntypedModule, + throwIfEventEmitterTypeIsUnsupported, + throwIfEventEmitterEventTypeIsUnsupported, + throwIfModuleTypeIsUnsupported, + throwIfMoreThanOneModuleInterfaceParserError, + throwIfUnsupportedFunctionParamTypeAnnotationParserError, + throwIfArrayElementTypeAnnotationIsUnsupported, + throwIfIncorrectModuleRegistryCallArgument, + throwIfPartialNotAnnotatingTypeParameter, + throwIfPartialWithMoreParameter, + throwIfMoreThanOneCodegenNativecommands, + throwIfConfigNotfound, + throwIfMoreThanOneConfig, + throwIfEventHasNoName, + throwIfBubblingTypeIsNull, + throwIfArgumentPropsAreNull, + throwIfTypeAliasIsNotInterface, +}; diff --git a/packages/react-native-codegen/lib/parsers/error-utils.js.flow b/packages/react-native-codegen/lib/parsers/error-utils.js.flow new file mode 100644 index 00000000000000..049920939922bc --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/error-utils.js.flow @@ -0,0 +1,424 @@ +/** + * 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 {NativeModuleTypeAnnotation} from '../CodegenSchema'; +import type {TypeDeclarationMap} from '../parsers/utils'; +import type {ParserType} from './errors'; +import type {Parser} from './parser'; + +const { + IncorrectModuleRegistryCallArgumentTypeParserError, + IncorrectModuleRegistryCallArityParserError, + IncorrectModuleRegistryCallTypeParameterParserError, + MisnamedModuleInterfaceParserError, + ModuleInterfaceNotFoundParserError, + MoreThanOneModuleInterfaceParserError, + MoreThanOneModuleRegistryCallsParserError, + UnsupportedArrayElementTypeAnnotationParserError, + UnsupportedFunctionParamTypeAnnotationParserError, + UnsupportedFunctionReturnTypeAnnotationParserError, + UnsupportedModuleEventEmitterPropertyParserError, + UnsupportedModuleEventEmitterTypePropertyParserError, + UnsupportedModulePropertyParserError, + UnsupportedObjectPropertyValueTypeAnnotationParserError, + UntypedModuleRegistryCallParserError, + UnusedModuleInterfaceParserError, +} = require('./errors'); + +function throwIfModuleInterfaceIsMisnamed( + nativeModuleName: string, + moduleSpecId: $FlowFixMe, + parserType: ParserType, +) { + if (moduleSpecId.name !== 'Spec') { + throw new MisnamedModuleInterfaceParserError( + nativeModuleName, + moduleSpecId, + parserType, + ); + } +} + +function throwIfModuleInterfaceNotFound( + numberOfModuleSpecs: number, + nativeModuleName: string, + ast: $FlowFixMe, + parserType: ParserType, +) { + if (numberOfModuleSpecs === 0) { + throw new ModuleInterfaceNotFoundParserError( + nativeModuleName, + ast, + parserType, + ); + } +} + +function throwIfMoreThanOneModuleRegistryCalls( + hasteModuleName: string, + callExpressions: $FlowFixMe, + callExpressionsLength: number, +) { + if (callExpressions.length > 1) { + throw new MoreThanOneModuleRegistryCallsParserError( + hasteModuleName, + callExpressions, + callExpressionsLength, + ); + } +} + +function throwIfUnusedModuleInterfaceParserError( + nativeModuleName: string, + moduleSpec: $FlowFixMe, + callExpressions: $FlowFixMe, +) { + if (callExpressions.length === 0) { + throw new UnusedModuleInterfaceParserError(nativeModuleName, moduleSpec); + } +} + +function throwIfWrongNumberOfCallExpressionArgs( + nativeModuleName: string, + flowCallExpression: $FlowFixMe, + methodName: string, + numberOfCallExpressionArgs: number, +) { + if (numberOfCallExpressionArgs !== 1) { + throw new IncorrectModuleRegistryCallArityParserError( + nativeModuleName, + flowCallExpression, + methodName, + numberOfCallExpressionArgs, + ); + } +} + +function throwIfIncorrectModuleRegistryCallTypeParameterParserError( + nativeModuleName: string, + typeArguments: $FlowFixMe, + methodName: string, + moduleName: string, + parser: Parser, +) { + function throwError() { + throw new IncorrectModuleRegistryCallTypeParameterParserError( + nativeModuleName, + typeArguments, + methodName, + moduleName, + ); + } + + if (parser.checkIfInvalidModule(typeArguments)) { + throwError(); + } +} + +function throwIfUnsupportedFunctionReturnTypeAnnotationParserError( + nativeModuleName: string, + returnTypeAnnotation: $FlowFixMe, + invalidReturnType: string, + cxxOnly: boolean, + returnType: string, +) { + if (!cxxOnly && returnType === 'FunctionTypeAnnotation') { + throw new UnsupportedFunctionReturnTypeAnnotationParserError( + nativeModuleName, + returnTypeAnnotation.returnType, + 'FunctionTypeAnnotation', + ); + } +} + +function throwIfUntypedModule( + typeArguments: $FlowFixMe, + hasteModuleName: string, + callExpression: $FlowFixMe, + methodName: string, + moduleName: string, +) { + if (typeArguments == null) { + throw new UntypedModuleRegistryCallParserError( + hasteModuleName, + callExpression, + methodName, + moduleName, + ); + } +} + +function throwIfEventEmitterTypeIsUnsupported( + nativeModuleName: string, + propertyName: string, + propertyValueType: string, + parser: Parser, + nullable: boolean, + untyped: boolean, + cxxOnly: boolean, +) { + if (nullable || untyped || !cxxOnly) { + throw new UnsupportedModuleEventEmitterPropertyParserError( + nativeModuleName, + propertyName, + propertyValueType, + parser.language(), + nullable, + untyped, + cxxOnly, + ); + } +} + +function throwIfEventEmitterEventTypeIsUnsupported( + nativeModuleName: string, + propertyName: string, + propertyValueType: string, + parser: Parser, + nullable: boolean, +) { + if (nullable) { + throw new UnsupportedModuleEventEmitterTypePropertyParserError( + nativeModuleName, + propertyName, + propertyValueType, + parser.language(), + nullable, + ); + } +} + +function throwIfModuleTypeIsUnsupported( + nativeModuleName: string, + propertyValue: $FlowFixMe, + propertyName: string, + propertyValueType: string, + parser: Parser, +) { + if (!parser.functionTypeAnnotation(propertyValueType)) { + throw new UnsupportedModulePropertyParserError( + nativeModuleName, + propertyValue, + propertyName, + propertyValueType, + parser.language(), + ); + } +} + +const UnsupportedObjectPropertyTypeToInvalidPropertyValueTypeMap = { + FunctionTypeAnnotation: 'FunctionTypeAnnotation', + VoidTypeAnnotation: 'void', + PromiseTypeAnnotation: 'Promise', +}; + +function throwIfPropertyValueTypeIsUnsupported( + moduleName: string, + propertyValue: $FlowFixMe, + propertyKey: string, + type: string, +) { + const invalidPropertyValueType = + // $FlowFixMe[invalid-computed-prop] + UnsupportedObjectPropertyTypeToInvalidPropertyValueTypeMap[type]; + + throw new UnsupportedObjectPropertyValueTypeAnnotationParserError( + moduleName, + propertyValue, + propertyKey, + invalidPropertyValueType, + ); +} + +function throwIfMoreThanOneModuleInterfaceParserError( + nativeModuleName: string, + moduleSpecs: $ReadOnlyArray<$FlowFixMe>, + parserType: ParserType, +) { + if (moduleSpecs.length > 1) { + throw new MoreThanOneModuleInterfaceParserError( + nativeModuleName, + moduleSpecs, + moduleSpecs.map(node => node.id.name), + parserType, + ); + } +} + +function throwIfUnsupportedFunctionParamTypeAnnotationParserError( + nativeModuleName: string, + languageParamTypeAnnotation: $FlowFixMe, + paramName: string, + paramTypeAnnotationType: NativeModuleTypeAnnotation['type'], +) { + throw new UnsupportedFunctionParamTypeAnnotationParserError( + nativeModuleName, + languageParamTypeAnnotation, + paramName, + paramTypeAnnotationType, + ); +} + +function throwIfArrayElementTypeAnnotationIsUnsupported( + hasteModuleName: string, + flowElementType: $FlowFixMe, + flowArrayType: 'Array' | '$ReadOnlyArray' | 'ReadonlyArray', + type: string, +) { + const TypeMap = { + FunctionTypeAnnotation: 'FunctionTypeAnnotation', + VoidTypeAnnotation: 'void', + PromiseTypeAnnotation: 'Promise', + // TODO: Added as a work-around for now until TupleTypeAnnotation are fully supported in both flow and TS + // Right now they are partially treated as UnionTypeAnnotation + UnionTypeAnnotation: 'UnionTypeAnnotation', + }; + + if (type in TypeMap) { + throw new UnsupportedArrayElementTypeAnnotationParserError( + hasteModuleName, + flowElementType, + flowArrayType, + // $FlowFixMe[invalid-computed-prop] + TypeMap[type], + ); + } +} + +function throwIfIncorrectModuleRegistryCallArgument( + nativeModuleName: string, + callExpressionArg: $FlowFixMe, + methodName: string, +) { + if ( + callExpressionArg.type !== 'StringLiteral' && + callExpressionArg.type !== 'Literal' + ) { + const {type} = callExpressionArg; + throw new IncorrectModuleRegistryCallArgumentTypeParserError( + nativeModuleName, + callExpressionArg, + methodName, + type, + ); + } +} + +function throwIfPartialNotAnnotatingTypeParameter( + typeAnnotation: $FlowFixMe, + types: TypeDeclarationMap, + parser: Parser, +) { + const annotatedElement = parser.extractAnnotatedElement( + typeAnnotation, + types, + ); + + if (!annotatedElement) { + throw new Error('Partials only support annotating a type parameter.'); + } +} + +function throwIfPartialWithMoreParameter(typeAnnotation: $FlowFixMe) { + if (typeAnnotation.typeParameters.params.length !== 1) { + throw new Error('Partials only support annotating exactly one parameter.'); + } +} + +function throwIfMoreThanOneCodegenNativecommands( + commandsTypeNames: $ReadOnlyArray<$FlowFixMe>, +) { + if (commandsTypeNames.length > 1) { + throw new Error('codegenNativeCommands may only be called once in a file'); + } +} + +function throwIfConfigNotfound(foundConfigs: Array<{[string]: string}>) { + if (foundConfigs.length === 0) { + throw new Error('Could not find component config for native component'); + } +} + +function throwIfMoreThanOneConfig(foundConfigs: Array<{[string]: string}>) { + if (foundConfigs.length > 1) { + throw new Error('Only one component is supported per file'); + } +} + +function throwIfEventHasNoName(typeAnnotation: $FlowFixMe, parser: Parser) { + const name = + parser.language() === 'Flow' ? typeAnnotation.id : typeAnnotation.typeName; + + if (!name) { + throw new Error("typeAnnotation of event doesn't have a name"); + } +} + +function throwIfBubblingTypeIsNull( + bubblingType: ?('direct' | 'bubble'), + eventName: string, +): 'direct' | 'bubble' { + if (!bubblingType) { + throw new Error( + `Unable to determine event bubbling type for "${eventName}"`, + ); + } + + return bubblingType; +} + +function throwIfArgumentPropsAreNull( + argumentProps: ?$ReadOnlyArray<$FlowFixMe>, + eventName: string, +): $ReadOnlyArray<$FlowFixMe> { + if (!argumentProps) { + throw new Error(`Unable to determine event arguments for "${eventName}"`); + } + + return argumentProps; +} + +function throwIfTypeAliasIsNotInterface(typeAlias: $FlowFixMe, parser: Parser) { + if (typeAlias.type !== parser.interfaceDeclaration) { + throw new Error( + `The type argument for codegenNativeCommands must be an interface, received ${typeAlias.type}`, + ); + } +} + +module.exports = { + throwIfModuleInterfaceIsMisnamed, + throwIfUnsupportedFunctionReturnTypeAnnotationParserError, + throwIfModuleInterfaceNotFound, + throwIfMoreThanOneModuleRegistryCalls, + throwIfPropertyValueTypeIsUnsupported, + throwIfUnusedModuleInterfaceParserError, + throwIfWrongNumberOfCallExpressionArgs, + throwIfIncorrectModuleRegistryCallTypeParameterParserError, + throwIfUntypedModule, + throwIfEventEmitterTypeIsUnsupported, + throwIfEventEmitterEventTypeIsUnsupported, + throwIfModuleTypeIsUnsupported, + throwIfMoreThanOneModuleInterfaceParserError, + throwIfUnsupportedFunctionParamTypeAnnotationParserError, + throwIfArrayElementTypeAnnotationIsUnsupported, + throwIfIncorrectModuleRegistryCallArgument, + throwIfPartialNotAnnotatingTypeParameter, + throwIfPartialWithMoreParameter, + throwIfMoreThanOneCodegenNativecommands, + throwIfConfigNotfound, + throwIfMoreThanOneConfig, + throwIfEventHasNoName, + throwIfBubblingTypeIsNull, + throwIfArgumentPropsAreNull, + throwIfTypeAliasIsNotInterface, +}; diff --git a/packages/react-native-codegen/lib/parsers/errors.d.ts b/packages/react-native-codegen/lib/parsers/errors.d.ts new file mode 100644 index 00000000000000..c99bd85e0d3f3d --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/errors.d.ts @@ -0,0 +1,10 @@ +/** + * 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. + */ + +export type ParserType = 'Flow' | 'TypeScript'; + +export declare class ParserError extends Error {} diff --git a/packages/react-native-codegen/lib/parsers/errors.js b/packages/react-native-codegen/lib/parsers/errors.js new file mode 100644 index 00000000000000..036529d43c2747 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/errors.js @@ -0,0 +1,371 @@ +/** + * 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'; + +class ParserError extends Error { + constructor(nativeModuleName, astNodeOrNodes, message) { + super(`Module ${nativeModuleName}: ${message}`); + this.nodes = Array.isArray(astNodeOrNodes) + ? astNodeOrNodes + : [astNodeOrNodes]; + + // assign the error class name in your custom error (as a shortcut) + this.name = this.constructor.name; + + // capturing the stack trace keeps the reference to your error class + Error.captureStackTrace(this, this.constructor); + } +} +class MisnamedModuleInterfaceParserError extends ParserError { + constructor(nativeModuleName, id, language) { + super( + nativeModuleName, + id, + `All ${language} interfaces extending TurboModule must be called 'Spec'. Please rename ${language} interface '${id.name}' to 'Spec'.`, + ); + } +} +class ModuleInterfaceNotFoundParserError extends ParserError { + constructor(nativeModuleName, ast, language) { + super( + nativeModuleName, + ast, + `No ${language} interfaces extending TurboModule were detected in this NativeModule spec.`, + ); + } +} +class MoreThanOneModuleInterfaceParserError extends ParserError { + constructor(nativeModuleName, flowModuleInterfaces, names, language) { + const finalName = names[names.length - 1]; + const allButLastName = names.slice(0, -1); + const quote = x => `'${x}'`; + const nameStr = + allButLastName.map(quote).join(', ') + ', and ' + quote(finalName); + super( + nativeModuleName, + flowModuleInterfaces, + `Every NativeModule spec file must declare exactly one NativeModule ${language} interface. This file declares ${names.length}: ${nameStr}. Please remove the extraneous ${language} interface declarations.`, + ); + } +} +class UnsupportedModuleEventEmitterTypePropertyParserError extends ParserError { + constructor( + nativeModuleName, + propertyValue, + propertyName, + language, + nullable, + ) { + super( + nativeModuleName, + propertyValue, + `Property '${propertyName}' is an EventEmitter and must have a non nullable eventType`, + ); + } +} +class UnsupportedModuleEventEmitterPropertyParserError extends ParserError { + constructor( + nativeModuleName, + propertyValue, + propertyName, + language, + nullable, + untyped, + cxxOnly, + ) { + let message = `${language} interfaces extending TurboModule must only contain 'FunctionTypeAnnotation's or non nullable 'EventEmitter's. Further the EventEmitter property `; + if (nullable) { + message += `'${propertyValue}' must non nullable.`; + } else if (untyped) { + message += `'${propertyValue}' must have a concrete or void eventType.`; + } else if (cxxOnly) { + message += `'${propertyValue}' is only supported in C++ Turbo Modules.`; + } + super(nativeModuleName, propertyValue, message); + } +} +class UnsupportedModulePropertyParserError extends ParserError { + constructor( + nativeModuleName, + propertyValue, + propertyName, + invalidPropertyValueType, + language, + ) { + super( + nativeModuleName, + propertyValue, + `${language} interfaces extending TurboModule must only contain 'FunctionTypeAnnotation's or non nullable 'EventEmitter's. Property '${propertyName}' refers to a '${invalidPropertyValueType}'.`, + ); + } +} +class UnsupportedTypeAnnotationParserError extends ParserError { + constructor(nativeModuleName, typeAnnotation, language) { + super( + nativeModuleName, + typeAnnotation, + `${language} type annotation '${typeAnnotation.type}' is unsupported in NativeModule specs.`, + ); + this.typeAnnotationType = typeAnnotation.type; + } +} +class UnsupportedGenericParserError extends ParserError { + // +genericName: string; + constructor(nativeModuleName, genericTypeAnnotation, parser) { + const genericName = parser.getTypeAnnotationName(genericTypeAnnotation); + super( + nativeModuleName, + genericTypeAnnotation, + `Unrecognized generic type '${genericName}' in NativeModule spec.`, + ); + + // this.genericName = genericName; + } +} +class MissingTypeParameterGenericParserError extends ParserError { + constructor(nativeModuleName, genericTypeAnnotation, parser) { + const genericName = parser.getTypeAnnotationName(genericTypeAnnotation); + super( + nativeModuleName, + genericTypeAnnotation, + `Generic '${genericName}' must have type parameters.`, + ); + } +} +class MoreThanOneTypeParameterGenericParserError extends ParserError { + constructor(nativeModuleName, genericTypeAnnotation, parser) { + const genericName = parser.getTypeAnnotationName(genericTypeAnnotation); + super( + nativeModuleName, + genericTypeAnnotation, + `Generic '${genericName}' must have exactly one type parameter.`, + ); + } +} + +/** + * Array parsing errors + */ + +class UnsupportedArrayElementTypeAnnotationParserError extends ParserError { + constructor( + nativeModuleName, + arrayElementTypeAST, + arrayType, + invalidArrayElementType, + ) { + super( + nativeModuleName, + arrayElementTypeAST, + `${arrayType} element types cannot be '${invalidArrayElementType}'.`, + ); + } +} + +/** + * Object parsing errors + */ + +class UnsupportedObjectPropertyTypeAnnotationParserError extends ParserError { + constructor(nativeModuleName, propertyAST, invalidPropertyType, language) { + let message = `'ObjectTypeAnnotation' cannot contain '${invalidPropertyType}'.`; + if ( + invalidPropertyType === 'ObjectTypeSpreadProperty' && + language !== 'TypeScript' + ) { + message = "Object spread isn't supported in 'ObjectTypeAnnotation's."; + } + super(nativeModuleName, propertyAST, message); + } +} +class UnsupportedObjectPropertyValueTypeAnnotationParserError extends ParserError { + constructor( + nativeModuleName, + propertyValueAST, + propertyName, + invalidPropertyValueType, + ) { + super( + nativeModuleName, + propertyValueAST, + `Object property '${propertyName}' cannot have type '${invalidPropertyValueType}'.`, + ); + } +} +class UnsupportedObjectDirectRecursivePropertyParserError extends ParserError { + constructor(propertyName, propertyValueAST, nativeModuleName) { + super( + nativeModuleName, + propertyValueAST, + `Object property '${propertyName}' is direct recursive and must be nullable.`, + ); + } +} + +/** + * Function parsing errors + */ + +class UnnamedFunctionParamParserError extends ParserError { + constructor(functionParam, nativeModuleName) { + super( + nativeModuleName, + functionParam, + 'All function parameters must be named.', + ); + } +} +class UnsupportedFunctionParamTypeAnnotationParserError extends ParserError { + constructor( + nativeModuleName, + flowParamTypeAnnotation, + paramName, + invalidParamType, + ) { + super( + nativeModuleName, + flowParamTypeAnnotation, + `Function parameter '${paramName}' cannot have type '${invalidParamType}'.`, + ); + } +} +class UnsupportedFunctionReturnTypeAnnotationParserError extends ParserError { + constructor(nativeModuleName, flowReturnTypeAnnotation, invalidReturnType) { + super( + nativeModuleName, + flowReturnTypeAnnotation, + `Function return cannot have type '${invalidReturnType}'.`, + ); + } +} + +/** + * Enum parsing errors + */ + +class UnsupportedEnumDeclarationParserError extends ParserError { + constructor(nativeModuleName, arrayElementTypeAST, memberType) { + super( + nativeModuleName, + arrayElementTypeAST, + `Unexpected enum member type ${memberType}. Only string and number enum members are supported`, + ); + } +} + +/** + * Union parsing errors + */ + +class UnsupportedUnionTypeAnnotationParserError extends ParserError { + constructor(nativeModuleName, arrayElementTypeAST, types) { + super( + nativeModuleName, + arrayElementTypeAST, + `Union members must be of the same type, but multiple types were found ${types.join( + ', ', + )}'.`, + ); + } +} + +/** + * Module parsing errors + */ + +class UnusedModuleInterfaceParserError extends ParserError { + constructor(nativeModuleName, flowInterface) { + super( + nativeModuleName, + flowInterface, + "Unused NativeModule spec. Please load the NativeModule by calling TurboModuleRegistry.get('').", + ); + } +} +class MoreThanOneModuleRegistryCallsParserError extends ParserError { + constructor(nativeModuleName, flowCallExpressions, numCalls) { + super( + nativeModuleName, + flowCallExpressions, + `Every NativeModule spec file must contain exactly one NativeModule load. This file contains ${numCalls}. Please simplify this spec file, splitting it as necessary, to remove the extraneous loads.`, + ); + } +} +class UntypedModuleRegistryCallParserError extends ParserError { + constructor(nativeModuleName, flowCallExpression, methodName, moduleName) { + super( + nativeModuleName, + flowCallExpression, + `Please type this NativeModule load: TurboModuleRegistry.${methodName}('${moduleName}').`, + ); + } +} +class IncorrectModuleRegistryCallTypeParameterParserError extends ParserError { + constructor(nativeModuleName, flowTypeArguments, methodName, moduleName) { + super( + nativeModuleName, + flowTypeArguments, + `Please change these type arguments to reflect TurboModuleRegistry.${methodName}('${moduleName}').`, + ); + } +} +class IncorrectModuleRegistryCallArityParserError extends ParserError { + constructor( + nativeModuleName, + flowCallExpression, + methodName, + incorrectArity, + ) { + super( + nativeModuleName, + flowCallExpression, + `Please call TurboModuleRegistry.${methodName}() with exactly one argument. Detected ${incorrectArity}.`, + ); + } +} +class IncorrectModuleRegistryCallArgumentTypeParserError extends ParserError { + constructor(nativeModuleName, flowArgument, methodName, type) { + const a = /[aeiouy]/.test(type.toLowerCase()) ? 'an' : 'a'; + super( + nativeModuleName, + flowArgument, + `Please call TurboModuleRegistry.${methodName}() with a string literal. Detected ${a} '${type}'`, + ); + } +} +module.exports = { + ParserError, + MissingTypeParameterGenericParserError, + MoreThanOneTypeParameterGenericParserError, + MisnamedModuleInterfaceParserError, + ModuleInterfaceNotFoundParserError, + MoreThanOneModuleInterfaceParserError, + UnnamedFunctionParamParserError, + UnsupportedArrayElementTypeAnnotationParserError, + UnsupportedGenericParserError, + UnsupportedTypeAnnotationParserError, + UnsupportedFunctionParamTypeAnnotationParserError, + UnsupportedFunctionReturnTypeAnnotationParserError, + UnsupportedEnumDeclarationParserError, + UnsupportedUnionTypeAnnotationParserError, + UnsupportedModuleEventEmitterTypePropertyParserError, + UnsupportedModuleEventEmitterPropertyParserError, + UnsupportedModulePropertyParserError, + UnsupportedObjectPropertyTypeAnnotationParserError, + UnsupportedObjectPropertyValueTypeAnnotationParserError, + UnsupportedObjectDirectRecursivePropertyParserError, + UnusedModuleInterfaceParserError, + MoreThanOneModuleRegistryCallsParserError, + UntypedModuleRegistryCallParserError, + IncorrectModuleRegistryCallTypeParameterParserError, + IncorrectModuleRegistryCallArityParserError, + IncorrectModuleRegistryCallArgumentTypeParserError, +}; diff --git a/packages/react-native-codegen/lib/parsers/errors.js.flow b/packages/react-native-codegen/lib/parsers/errors.js.flow new file mode 100644 index 00000000000000..dde9cf45ac54a7 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/errors.js.flow @@ -0,0 +1,469 @@ +/** + * 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 {UnionTypeAnnotationMemberType} from '../CodegenSchema'; +import type {Parser} from './parser'; +export type ParserType = 'Flow' | 'TypeScript'; + +class ParserError extends Error { + nodes: $ReadOnlyArray<$FlowFixMe>; + constructor( + nativeModuleName: string, + astNodeOrNodes: $FlowFixMe, + message: string, + ) { + super(`Module ${nativeModuleName}: ${message}`); + + this.nodes = Array.isArray(astNodeOrNodes) + ? astNodeOrNodes + : [astNodeOrNodes]; + + // assign the error class name in your custom error (as a shortcut) + this.name = this.constructor.name; + + // capturing the stack trace keeps the reference to your error class + Error.captureStackTrace(this, this.constructor); + } +} +class MisnamedModuleInterfaceParserError extends ParserError { + constructor(nativeModuleName: string, id: $FlowFixMe, language: ParserType) { + super( + nativeModuleName, + id, + `All ${language} interfaces extending TurboModule must be called 'Spec'. Please rename ${language} interface '${id.name}' to 'Spec'.`, + ); + } +} + +class ModuleInterfaceNotFoundParserError extends ParserError { + constructor(nativeModuleName: string, ast: $FlowFixMe, language: ParserType) { + super( + nativeModuleName, + ast, + `No ${language} interfaces extending TurboModule were detected in this NativeModule spec.`, + ); + } +} + +class MoreThanOneModuleInterfaceParserError extends ParserError { + constructor( + nativeModuleName: string, + flowModuleInterfaces: $ReadOnlyArray<$FlowFixMe>, + names: $ReadOnlyArray, + language: ParserType, + ) { + const finalName = names[names.length - 1]; + const allButLastName = names.slice(0, -1); + const quote = (x: string) => `'${x}'`; + + const nameStr = + allButLastName.map(quote).join(', ') + ', and ' + quote(finalName); + + super( + nativeModuleName, + flowModuleInterfaces, + `Every NativeModule spec file must declare exactly one NativeModule ${language} interface. This file declares ${names.length}: ${nameStr}. Please remove the extraneous ${language} interface declarations.`, + ); + } +} + +class UnsupportedModuleEventEmitterTypePropertyParserError extends ParserError { + constructor( + nativeModuleName: string, + propertyValue: $FlowFixMe, + propertyName: string, + language: ParserType, + nullable: boolean, + ) { + super( + nativeModuleName, + propertyValue, + `Property '${propertyName}' is an EventEmitter and must have a non nullable eventType`, + ); + } +} + +class UnsupportedModuleEventEmitterPropertyParserError extends ParserError { + constructor( + nativeModuleName: string, + propertyValue: $FlowFixMe, + propertyName: string, + language: ParserType, + nullable: boolean, + untyped: boolean, + cxxOnly: boolean, + ) { + let message = `${language} interfaces extending TurboModule must only contain 'FunctionTypeAnnotation's or non nullable 'EventEmitter's. Further the EventEmitter property `; + if (nullable) { + message += `'${propertyValue}' must non nullable.`; + } else if (untyped) { + message += `'${propertyValue}' must have a concrete or void eventType.`; + } else if (cxxOnly) { + message += `'${propertyValue}' is only supported in C++ Turbo Modules.`; + } + super(nativeModuleName, propertyValue, message); + } +} + +class UnsupportedModulePropertyParserError extends ParserError { + constructor( + nativeModuleName: string, + propertyValue: $FlowFixMe, + propertyName: string, + invalidPropertyValueType: string, + language: ParserType, + ) { + super( + nativeModuleName, + propertyValue, + `${language} interfaces extending TurboModule must only contain 'FunctionTypeAnnotation's or non nullable 'EventEmitter's. Property '${propertyName}' refers to a '${invalidPropertyValueType}'.`, + ); + } +} + +class UnsupportedTypeAnnotationParserError extends ParserError { + +typeAnnotationType: string; + constructor( + nativeModuleName: string, + typeAnnotation: $FlowFixMe, + language: ParserType, + ) { + super( + nativeModuleName, + typeAnnotation, + `${language} type annotation '${typeAnnotation.type}' is unsupported in NativeModule specs.`, + ); + + this.typeAnnotationType = typeAnnotation.type; + } +} + +class UnsupportedGenericParserError extends ParserError { + // +genericName: string; + constructor( + nativeModuleName: string, + genericTypeAnnotation: $FlowFixMe, + parser: Parser, + ) { + const genericName = parser.getTypeAnnotationName(genericTypeAnnotation); + super( + nativeModuleName, + genericTypeAnnotation, + `Unrecognized generic type '${genericName}' in NativeModule spec.`, + ); + + // this.genericName = genericName; + } +} + +class MissingTypeParameterGenericParserError extends ParserError { + constructor( + nativeModuleName: string, + genericTypeAnnotation: $FlowFixMe, + parser: Parser, + ) { + const genericName = parser.getTypeAnnotationName(genericTypeAnnotation); + + super( + nativeModuleName, + genericTypeAnnotation, + `Generic '${genericName}' must have type parameters.`, + ); + } +} + +class MoreThanOneTypeParameterGenericParserError extends ParserError { + constructor( + nativeModuleName: string, + genericTypeAnnotation: $FlowFixMe, + parser: Parser, + ) { + const genericName = parser.getTypeAnnotationName(genericTypeAnnotation); + + super( + nativeModuleName, + genericTypeAnnotation, + `Generic '${genericName}' must have exactly one type parameter.`, + ); + } +} + +/** + * Array parsing errors + */ + +class UnsupportedArrayElementTypeAnnotationParserError extends ParserError { + constructor( + nativeModuleName: string, + arrayElementTypeAST: $FlowFixMe, + arrayType: 'Array' | '$ReadOnlyArray' | 'ReadonlyArray', + invalidArrayElementType: string, + ) { + super( + nativeModuleName, + arrayElementTypeAST, + `${arrayType} element types cannot be '${invalidArrayElementType}'.`, + ); + } +} + +/** + * Object parsing errors + */ + +class UnsupportedObjectPropertyTypeAnnotationParserError extends ParserError { + constructor( + nativeModuleName: string, + propertyAST: $FlowFixMe, + invalidPropertyType: string, + language: ParserType, + ) { + let message = `'ObjectTypeAnnotation' cannot contain '${invalidPropertyType}'.`; + + if ( + invalidPropertyType === 'ObjectTypeSpreadProperty' && + language !== 'TypeScript' + ) { + message = "Object spread isn't supported in 'ObjectTypeAnnotation's."; + } + + super(nativeModuleName, propertyAST, message); + } +} + +class UnsupportedObjectPropertyValueTypeAnnotationParserError extends ParserError { + constructor( + nativeModuleName: string, + propertyValueAST: $FlowFixMe, + propertyName: string, + invalidPropertyValueType: string, + ) { + super( + nativeModuleName, + propertyValueAST, + `Object property '${propertyName}' cannot have type '${invalidPropertyValueType}'.`, + ); + } +} + +class UnsupportedObjectDirectRecursivePropertyParserError extends ParserError { + constructor( + propertyName: string, + propertyValueAST: $FlowFixMe, + nativeModuleName: string, + ) { + super( + nativeModuleName, + propertyValueAST, + `Object property '${propertyName}' is direct recursive and must be nullable.`, + ); + } +} + +/** + * Function parsing errors + */ + +class UnnamedFunctionParamParserError extends ParserError { + constructor(functionParam: $FlowFixMe, nativeModuleName: string) { + super( + nativeModuleName, + functionParam, + 'All function parameters must be named.', + ); + } +} + +class UnsupportedFunctionParamTypeAnnotationParserError extends ParserError { + constructor( + nativeModuleName: string, + flowParamTypeAnnotation: $FlowFixMe, + paramName: string, + invalidParamType: string, + ) { + super( + nativeModuleName, + flowParamTypeAnnotation, + `Function parameter '${paramName}' cannot have type '${invalidParamType}'.`, + ); + } +} + +class UnsupportedFunctionReturnTypeAnnotationParserError extends ParserError { + constructor( + nativeModuleName: string, + flowReturnTypeAnnotation: $FlowFixMe, + invalidReturnType: string, + ) { + super( + nativeModuleName, + flowReturnTypeAnnotation, + `Function return cannot have type '${invalidReturnType}'.`, + ); + } +} + +/** + * Enum parsing errors + */ + +class UnsupportedEnumDeclarationParserError extends ParserError { + constructor( + nativeModuleName: string, + arrayElementTypeAST: $FlowFixMe, + memberType: string, + ) { + super( + nativeModuleName, + arrayElementTypeAST, + `Unexpected enum member type ${memberType}. Only string and number enum members are supported`, + ); + } +} + +/** + * Union parsing errors + */ + +class UnsupportedUnionTypeAnnotationParserError extends ParserError { + constructor( + nativeModuleName: string, + arrayElementTypeAST: $FlowFixMe, + types: UnionTypeAnnotationMemberType[], + ) { + super( + nativeModuleName, + arrayElementTypeAST, + `Union members must be of the same type, but multiple types were found ${types.join( + ', ', + )}'.`, + ); + } +} + +/** + * Module parsing errors + */ + +class UnusedModuleInterfaceParserError extends ParserError { + constructor(nativeModuleName: string, flowInterface: $FlowFixMe) { + super( + nativeModuleName, + flowInterface, + "Unused NativeModule spec. Please load the NativeModule by calling TurboModuleRegistry.get('').", + ); + } +} + +class MoreThanOneModuleRegistryCallsParserError extends ParserError { + constructor( + nativeModuleName: string, + flowCallExpressions: $FlowFixMe, + numCalls: number, + ) { + super( + nativeModuleName, + flowCallExpressions, + `Every NativeModule spec file must contain exactly one NativeModule load. This file contains ${numCalls}. Please simplify this spec file, splitting it as necessary, to remove the extraneous loads.`, + ); + } +} + +class UntypedModuleRegistryCallParserError extends ParserError { + constructor( + nativeModuleName: string, + flowCallExpression: $FlowFixMe, + methodName: string, + moduleName: string, + ) { + super( + nativeModuleName, + flowCallExpression, + `Please type this NativeModule load: TurboModuleRegistry.${methodName}('${moduleName}').`, + ); + } +} + +class IncorrectModuleRegistryCallTypeParameterParserError extends ParserError { + constructor( + nativeModuleName: string, + flowTypeArguments: $FlowFixMe, + methodName: string, + moduleName: string, + ) { + super( + nativeModuleName, + flowTypeArguments, + `Please change these type arguments to reflect TurboModuleRegistry.${methodName}('${moduleName}').`, + ); + } +} + +class IncorrectModuleRegistryCallArityParserError extends ParserError { + constructor( + nativeModuleName: string, + flowCallExpression: $FlowFixMe, + methodName: string, + incorrectArity: number, + ) { + super( + nativeModuleName, + flowCallExpression, + `Please call TurboModuleRegistry.${methodName}() with exactly one argument. Detected ${incorrectArity}.`, + ); + } +} + +class IncorrectModuleRegistryCallArgumentTypeParserError extends ParserError { + constructor( + nativeModuleName: string, + flowArgument: $FlowFixMe, + methodName: string, + type: string, + ) { + const a = /[aeiouy]/.test(type.toLowerCase()) ? 'an' : 'a'; + super( + nativeModuleName, + flowArgument, + `Please call TurboModuleRegistry.${methodName}() with a string literal. Detected ${a} '${type}'`, + ); + } +} + +module.exports = { + ParserError, + MissingTypeParameterGenericParserError, + MoreThanOneTypeParameterGenericParserError, + MisnamedModuleInterfaceParserError, + ModuleInterfaceNotFoundParserError, + MoreThanOneModuleInterfaceParserError, + UnnamedFunctionParamParserError, + UnsupportedArrayElementTypeAnnotationParserError, + UnsupportedGenericParserError, + UnsupportedTypeAnnotationParserError, + UnsupportedFunctionParamTypeAnnotationParserError, + UnsupportedFunctionReturnTypeAnnotationParserError, + UnsupportedEnumDeclarationParserError, + UnsupportedUnionTypeAnnotationParserError, + UnsupportedModuleEventEmitterTypePropertyParserError, + UnsupportedModuleEventEmitterPropertyParserError, + UnsupportedModulePropertyParserError, + UnsupportedObjectPropertyTypeAnnotationParserError, + UnsupportedObjectPropertyValueTypeAnnotationParserError, + UnsupportedObjectDirectRecursivePropertyParserError, + UnusedModuleInterfaceParserError, + MoreThanOneModuleRegistryCallsParserError, + UntypedModuleRegistryCallParserError, + IncorrectModuleRegistryCallTypeParameterParserError, + IncorrectModuleRegistryCallArityParserError, + IncorrectModuleRegistryCallArgumentTypeParserError, +}; diff --git a/packages/react-native-codegen/lib/parsers/flow/components/__test_fixtures__/failures.js b/packages/react-native-codegen/lib/parsers/flow/components/__test_fixtures__/failures.js new file mode 100644 index 00000000000000..24f2cb1e033ba5 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/flow/components/__test_fixtures__/failures.js @@ -0,0 +1,583 @@ +/** + * 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 + */ + +// @licenselint-loose-mode + +'use strict'; + +const COMMANDS_DEFINED_INLINE = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +const codegenNativeCommands = require('codegenNativeCommands'); +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {Int32} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +export type ModuleProps = $ReadOnly<{| + ...ViewProps, + // No props +|}>; + +export const Commands = codegenNativeCommands<{ + +hotspotUpdate: (ref: React.Ref<'RCTView'>, x: Int32, y: Int32) => void, +}>({ + supportedCommands: ['hotspotUpdate'], +}); + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; +const COMMANDS_DEFINED_MULTIPLE_TIMES = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +const codegenNativeCommands = require('codegenNativeCommands'); +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {Int32} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +interface NativeCommands { + +hotspotUpdate: (viewRef: React.Ref<'RCTView'>, x: Int32, y: Int32) => void; +} + +export type ModuleProps = $ReadOnly<{| + ...ViewProps, + // No props or events +|}>; + +export const Commands = codegenNativeCommands({ + supportedCommands: ['hotspotUpdate'], +}); +export const Commands2 = codegenNativeCommands({ + supportedCommands: ['hotspotUpdate'], +}); + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; +const COMMANDS_DEFINED_WITHOUT_REF = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +const codegenNativeCommands = require('codegenNativeCommands'); +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {Int32} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +interface NativeCommands { + +hotspotUpdate: (x: Int32, y: Int32) => void; +} + +export type ModuleProps = $ReadOnly<{| + ...ViewProps, + // No props or events +|}>; + +export const Commands = codegenNativeCommands({ + supportedCommands: ['hotspotUpdate'], +}); + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; +const COMMANDS_DEFINED_WITH_NULLABLE_REF = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +const codegenNativeCommands = require('codegenNativeCommands'); +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {Int32} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +interface NativeCommands { + +hotspotUpdate: (viewRef: ?React.Ref<'RCTView'>, x: Int32, y: Int32) => void; +} + +export type ModuleProps = $ReadOnly<{| + ...ViewProps, + // No props or events +|}>; + +export const Commands = codegenNativeCommands({ + supportedCommands: ['hotspotUpdate'], +}); + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; +const COMMANDS_DEFINED_WITH_MISMATCHED_METHOD_NAMES = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +const codegenNativeCommands = require('codegenNativeCommands'); +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {Int32} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +interface NativeCommands { + +hotspotUpdate: (viewRef: React.Ref<'RCTView'>, x: Int32, y: Int32) => void; + +scrollTo: ( + viewRef: React.Ref<'RCTView'>, + y: Int32, + animated: boolean, + ) => void; +} + +export type ModuleProps = $ReadOnly<{| + ...ViewProps, + // No props or events +|}>; + +export const Commands = codegenNativeCommands({ + supportedCommands: ['scrollTo'], +}); + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; +const COMMANDS_DEFINED_WITHOUT_METHOD_NAMES = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +const codegenNativeCommands = require('codegenNativeCommands'); +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {Int32} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +interface NativeCommands { + +hotspotUpdate: (viewRef: React.Ref<'RCTView'>, x: Int32, y: Int32) => void; + +scrollTo: ( + viewRef: React.Ref<'RCTView'>, + y: Int32, + animated: boolean, + ) => void; +} + +export type ModuleProps = $ReadOnly<{| + ...ViewProps, + // No props or events +|}>; + +export const Commands = codegenNativeCommands(); + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; +const NULLABLE_WITH_DEFAULT = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {WithDefault, Float} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +export type ModuleProps = $ReadOnly<{| + ...ViewProps, + nullable_with_default: ?WithDefault, +|}>; + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; +const NON_OPTIONAL_KEY_WITH_DEFAULT_VALUE = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {WithDefault, Float} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +export type ModuleProps = $ReadOnly<{| + ...ViewProps, + required_key_with_default: WithDefault, +|}>; + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; +const PROPS_CONFLICT_NAMES = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +export type ModuleProps = $ReadOnly<{| + ...ViewProps, + isEnabled: string, + + isEnabled: boolean, +|}>; + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; +const PROPS_CONFLICT_WITH_SPREAD_PROPS = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +type PropsInFile = $ReadOnly<{| + isEnabled: boolean, +|}>; + +export type ModuleProps = $ReadOnly<{| + ...ViewProps, + + ...PropsInFile, + isEnabled: boolean, +|}>; + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; +const PROPS_SPREAD_CONFLICTS_WITH_PROPS = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +type PropsInFile = $ReadOnly<{| + isEnabled: boolean, +|}>; + +export type ModuleProps = $ReadOnly<{| + ...ViewProps, + + isEnabled: boolean, + ...PropsInFile, +|}>; + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; +const PROP_NUMBER_TYPE = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +export type ModuleProps = $ReadOnly<{| + ...ViewProps, + + someProp: number +|}>; + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; +const PROP_MIXED_ENUM = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +export type ModuleProps = $ReadOnly<{| + ...ViewProps, + + someProp?: WithDefault<'foo' | 1, 1> +|}>; + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; +const PROP_ENUM_BOOLEAN = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +export type ModuleProps = $ReadOnly<{| + ...ViewProps, + + someProp?: WithDefault +|}>; + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; +const PROP_ARRAY_MIXED_ENUM = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +export type ModuleProps = $ReadOnly<{| + ...ViewProps, + + someProp?: WithDefault<$ReadOnlyArray<'foo' | 1>, 1> +|}>; + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; +const PROP_ARRAY_ENUM_BOOLEAN = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +export type ModuleProps = $ReadOnly<{| + ...ViewProps, + + someProp?: WithDefault<$ReadOnlyArray, false> +|}>; + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; +const PROP_ARRAY_ENUM_INT = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +export type ModuleProps = $ReadOnly<{| + ...ViewProps, + + someProp?: WithDefault<$ReadOnlyArray<0 | 1>, 0> +|}>; + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; +module.exports = { + COMMANDS_DEFINED_INLINE, + COMMANDS_DEFINED_MULTIPLE_TIMES, + COMMANDS_DEFINED_WITH_MISMATCHED_METHOD_NAMES, + COMMANDS_DEFINED_WITHOUT_METHOD_NAMES, + COMMANDS_DEFINED_WITHOUT_REF, + COMMANDS_DEFINED_WITH_NULLABLE_REF, + NULLABLE_WITH_DEFAULT, + NON_OPTIONAL_KEY_WITH_DEFAULT_VALUE, + PROPS_CONFLICT_NAMES, + PROPS_CONFLICT_WITH_SPREAD_PROPS, + PROPS_SPREAD_CONFLICTS_WITH_PROPS, + PROP_NUMBER_TYPE, + PROP_MIXED_ENUM, + PROP_ENUM_BOOLEAN, + PROP_ARRAY_MIXED_ENUM, + PROP_ARRAY_ENUM_BOOLEAN, + PROP_ARRAY_ENUM_INT, +}; diff --git a/packages/react-native-codegen/lib/parsers/flow/components/__test_fixtures__/failures.js.flow b/packages/react-native-codegen/lib/parsers/flow/components/__test_fixtures__/failures.js.flow new file mode 100644 index 00000000000000..1d7072636a48a8 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/flow/components/__test_fixtures__/failures.js.flow @@ -0,0 +1,600 @@ +/** + * 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-local + * @format + */ + +// @licenselint-loose-mode + +'use strict'; + +const COMMANDS_DEFINED_INLINE = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +const codegenNativeCommands = require('codegenNativeCommands'); +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {Int32} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +export type ModuleProps = $ReadOnly<{| + ...ViewProps, + // No props +|}>; + +export const Commands = codegenNativeCommands<{ + +hotspotUpdate: (ref: React.Ref<'RCTView'>, x: Int32, y: Int32) => void, +}>({ + supportedCommands: ['hotspotUpdate'], +}); + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; + +const COMMANDS_DEFINED_MULTIPLE_TIMES = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +const codegenNativeCommands = require('codegenNativeCommands'); +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {Int32} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +interface NativeCommands { + +hotspotUpdate: (viewRef: React.Ref<'RCTView'>, x: Int32, y: Int32) => void; +} + +export type ModuleProps = $ReadOnly<{| + ...ViewProps, + // No props or events +|}>; + +export const Commands = codegenNativeCommands({ + supportedCommands: ['hotspotUpdate'], +}); +export const Commands2 = codegenNativeCommands({ + supportedCommands: ['hotspotUpdate'], +}); + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; + +const COMMANDS_DEFINED_WITHOUT_REF = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +const codegenNativeCommands = require('codegenNativeCommands'); +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {Int32} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +interface NativeCommands { + +hotspotUpdate: (x: Int32, y: Int32) => void; +} + +export type ModuleProps = $ReadOnly<{| + ...ViewProps, + // No props or events +|}>; + +export const Commands = codegenNativeCommands({ + supportedCommands: ['hotspotUpdate'], +}); + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; + +const COMMANDS_DEFINED_WITH_NULLABLE_REF = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +const codegenNativeCommands = require('codegenNativeCommands'); +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {Int32} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +interface NativeCommands { + +hotspotUpdate: (viewRef: ?React.Ref<'RCTView'>, x: Int32, y: Int32) => void; +} + +export type ModuleProps = $ReadOnly<{| + ...ViewProps, + // No props or events +|}>; + +export const Commands = codegenNativeCommands({ + supportedCommands: ['hotspotUpdate'], +}); + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; + +const COMMANDS_DEFINED_WITH_MISMATCHED_METHOD_NAMES = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +const codegenNativeCommands = require('codegenNativeCommands'); +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {Int32} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +interface NativeCommands { + +hotspotUpdate: (viewRef: React.Ref<'RCTView'>, x: Int32, y: Int32) => void; + +scrollTo: ( + viewRef: React.Ref<'RCTView'>, + y: Int32, + animated: boolean, + ) => void; +} + +export type ModuleProps = $ReadOnly<{| + ...ViewProps, + // No props or events +|}>; + +export const Commands = codegenNativeCommands({ + supportedCommands: ['scrollTo'], +}); + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; + +const COMMANDS_DEFINED_WITHOUT_METHOD_NAMES = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +const codegenNativeCommands = require('codegenNativeCommands'); +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {Int32} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +interface NativeCommands { + +hotspotUpdate: (viewRef: React.Ref<'RCTView'>, x: Int32, y: Int32) => void; + +scrollTo: ( + viewRef: React.Ref<'RCTView'>, + y: Int32, + animated: boolean, + ) => void; +} + +export type ModuleProps = $ReadOnly<{| + ...ViewProps, + // No props or events +|}>; + +export const Commands = codegenNativeCommands(); + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; + +const NULLABLE_WITH_DEFAULT = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {WithDefault, Float} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +export type ModuleProps = $ReadOnly<{| + ...ViewProps, + nullable_with_default: ?WithDefault, +|}>; + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; + +const NON_OPTIONAL_KEY_WITH_DEFAULT_VALUE = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {WithDefault, Float} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +export type ModuleProps = $ReadOnly<{| + ...ViewProps, + required_key_with_default: WithDefault, +|}>; + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; + +const PROPS_CONFLICT_NAMES = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +export type ModuleProps = $ReadOnly<{| + ...ViewProps, + isEnabled: string, + + isEnabled: boolean, +|}>; + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; + +const PROPS_CONFLICT_WITH_SPREAD_PROPS = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +type PropsInFile = $ReadOnly<{| + isEnabled: boolean, +|}>; + +export type ModuleProps = $ReadOnly<{| + ...ViewProps, + + ...PropsInFile, + isEnabled: boolean, +|}>; + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; + +const PROPS_SPREAD_CONFLICTS_WITH_PROPS = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +type PropsInFile = $ReadOnly<{| + isEnabled: boolean, +|}>; + +export type ModuleProps = $ReadOnly<{| + ...ViewProps, + + isEnabled: boolean, + ...PropsInFile, +|}>; + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; + +const PROP_NUMBER_TYPE = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +export type ModuleProps = $ReadOnly<{| + ...ViewProps, + + someProp: number +|}>; + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; + +const PROP_MIXED_ENUM = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +export type ModuleProps = $ReadOnly<{| + ...ViewProps, + + someProp?: WithDefault<'foo' | 1, 1> +|}>; + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; + +const PROP_ENUM_BOOLEAN = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +export type ModuleProps = $ReadOnly<{| + ...ViewProps, + + someProp?: WithDefault +|}>; + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; + +const PROP_ARRAY_MIXED_ENUM = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +export type ModuleProps = $ReadOnly<{| + ...ViewProps, + + someProp?: WithDefault<$ReadOnlyArray<'foo' | 1>, 1> +|}>; + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; + +const PROP_ARRAY_ENUM_BOOLEAN = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +export type ModuleProps = $ReadOnly<{| + ...ViewProps, + + someProp?: WithDefault<$ReadOnlyArray, false> +|}>; + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; + +const PROP_ARRAY_ENUM_INT = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +export type ModuleProps = $ReadOnly<{| + ...ViewProps, + + someProp?: WithDefault<$ReadOnlyArray<0 | 1>, 0> +|}>; + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; + +module.exports = { + COMMANDS_DEFINED_INLINE, + COMMANDS_DEFINED_MULTIPLE_TIMES, + COMMANDS_DEFINED_WITH_MISMATCHED_METHOD_NAMES, + COMMANDS_DEFINED_WITHOUT_METHOD_NAMES, + COMMANDS_DEFINED_WITHOUT_REF, + COMMANDS_DEFINED_WITH_NULLABLE_REF, + NULLABLE_WITH_DEFAULT, + NON_OPTIONAL_KEY_WITH_DEFAULT_VALUE, + PROPS_CONFLICT_NAMES, + PROPS_CONFLICT_WITH_SPREAD_PROPS, + PROPS_SPREAD_CONFLICTS_WITH_PROPS, + PROP_NUMBER_TYPE, + PROP_MIXED_ENUM, + PROP_ENUM_BOOLEAN, + PROP_ARRAY_MIXED_ENUM, + PROP_ARRAY_ENUM_BOOLEAN, + PROP_ARRAY_ENUM_INT, +}; diff --git a/packages/react-native-codegen/lib/parsers/flow/components/__test_fixtures__/fixtures.js b/packages/react-native-codegen/lib/parsers/flow/components/__test_fixtures__/fixtures.js new file mode 100644 index 00000000000000..de29b12b6a598e --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/flow/components/__test_fixtures__/fixtures.js @@ -0,0 +1,1080 @@ +/** + * 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 + */ + +// @licenselint-loose-mode + +'use strict'; + +const EVENT_DEFINITION = ` + boolean_required: boolean, + boolean_optional_key?: boolean, + boolean_optional_value: ?boolean, + boolean_optional_both?: ?boolean, + + string_required: string, + string_optional_key?: string, + string_optional_value: ?string, + string_optional_both?: ?string, + + double_required: Double, + double_optional_key?: Double, + double_optional_value: ?Double, + double_optional_both?: ?Double, + + float_required: Float, + float_optional_key?: Float, + float_optional_value: ?Float, + float_optional_both?: ?Float, + + int32_required: Int32, + int32_optional_key?: Int32, + int32_optional_value: ?Int32, + int32_optional_both?: ?Int32, + + enum_required: ('small' | 'large'), + enum_optional_key?: ('small' | 'large'), + enum_optional_value: ?('small' | 'large'), + enum_optional_both?: ?('small' | 'large'), + + object_required: { + boolean_required: boolean, + }, + + object_optional_key?: { + string_optional_key?: string, + }, + + object_optional_value: ?{ + float_optional_value: ?Float, + }, + + object_optional_both?: ?{ + int32_optional_both?: ?Int32, + }, + + object_required_nested_2_layers: { + object_optional_nested_1_layer?: ?{ + boolean_required: Int32, + string_optional_key?: string, + double_optional_value: ?Double, + float_optional_value: ?Float, + int32_optional_both?: ?Int32, + } + }, + + object_readonly_required: $ReadOnly<{ + boolean_required: boolean, + }>, + + object_readonly_optional_key?: $ReadOnly<{ + string_optional_key?: string, + }>, + + object_readonly_optional_value: ?$ReadOnly<{ + float_optional_value: ?Float, + }>, + + object_readonly_optional_both?: ?$ReadOnly<{ + int32_optional_both?: ?Int32, + }>, + + boolean_array_required: $ReadOnlyArray, + boolean_array_optional_key?: boolean[], + boolean_array_optional_value: ?$ReadOnlyArray, + boolean_array_optional_both?: ?boolean[], + + string_array_required: $ReadOnlyArray, + string_array_optional_key?: string[], + string_array_optional_value: ?$ReadOnlyArray, + string_array_optional_both?: ?string[], + + double_array_required: $ReadOnlyArray, + double_array_optional_key?: Double[], + double_array_optional_value: ?$ReadOnlyArray, + double_array_optional_both?: ?Double[], + + float_array_required: $ReadOnlyArray, + float_array_optional_key?: Float[], + float_array_optional_value: ?$ReadOnlyArray, + float_array_optional_both?: ?Float[], + + int32_array_required: $ReadOnlyArray, + int32_array_optional_key?: Int32[], + int32_array_optional_value: ?$ReadOnlyArray, + int32_array_optional_both?: ?Int32[], + + enum_array_required: $ReadOnlyArray<('small' | 'large')>, + enum_array_optional_key?: ('small' | 'large')[], + enum_array_optional_value: ?$ReadOnlyArray<('small' | 'large')>, + enum_array_optional_both?: ?('small' | 'large')[], + + object_array_required: $ReadOnlyArray<{ + boolean_required: boolean, + }>, + + object_array_optional_key?: { + string_optional_key?: string, + }[], + + object_array_optional_value: ?$ReadOnlyArray<{ + float_optional_value: ?Float, + }>, + + object_array_optional_both?: ?{ + int32_optional_both?: ?Int32, + }[], + + int32_array_array_required: $ReadOnlyArray<$ReadOnlyArray>, + int32_array_array_optional_key?: Int32[][], + int32_array_array_optional_value: ?$ReadOnlyArray<$ReadOnlyArray>, + int32_array_array_optional_both?: ?Int32[][], +`; +const ONE_OF_EACH_PROP_EVENT_DEFAULT_AND_OPTIONS = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type { + BubblingEventHandler, + DirectEventHandler, + WithDefault, +} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +type ModuleProps = $ReadOnly<{| + ...ViewProps, + + // Props + boolean_default_true_optional_both?: WithDefault, + + // Events + onDirectEventDefinedInlineNull: DirectEventHandler, + onBubblingEventDefinedInlineNull: BubblingEventHandler, +|}>; + +export default (codegenNativeComponent('Module', { + interfaceOnly: true, + paperComponentName: 'RCTModule', +}): HostComponent); +`; +const ONE_OF_EACH_PROP_EVENT_DEFAULT_AND_OPTIONS_NO_CAST = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type { + BubblingEventHandler, + DirectEventHandler, + WithDefault, +} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +type ModuleProps = $ReadOnly<{| + ...ViewProps, + + // Props + boolean_default_true_optional_both?: WithDefault, + + // Events + onDirectEventDefinedInlineNull: DirectEventHandler, + onBubblingEventDefinedInlineNull: BubblingEventHandler, +|}>; + +export default codegenNativeComponent('Module', { + interfaceOnly: true, + excludedPlatforms: ['android'], + paperComponentName: 'RCTModule', +}); +`; +const NO_PROPS_EVENTS_ONLY_DEPRECATED_VIEW_CONFIG_NAME_OPTION = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +type ModuleProps = $ReadOnly<{| + ...ViewProps, +|}>; + +export default (codegenNativeComponent('Module', { + deprecatedViewConfigName: 'DeprecateModuleName', +}): HostComponent); +`; +const ALL_PROP_TYPES_NO_EVENTS = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {Int32, Double, Float, WithDefault, UnsafeMixed} from 'CodegenTypes'; +import type {ImageSource} from 'ImageSource'; +import type {ColorValue, ColorArrayValue, PointValue, EdgeInsetsValue, DimensionValue} from 'StyleSheetTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +type ModuleProps = $ReadOnly<{| + ...ViewProps, + + // Props + // Boolean props + boolean_required: boolean, + boolean_optional_key?: WithDefault, + boolean_optional_both?: WithDefault, + + // Boolean props, null default + boolean_null_optional_key?: WithDefault, + boolean_null_optional_both?: WithDefault, + + // String props + string_required: string, + string_optional_key?: WithDefault, + string_optional_both?: WithDefault, + + // String props, null default + string_null_optional_key?: WithDefault, + string_null_optional_both?: WithDefault, + + // Stringish props + stringish_required: Stringish, + stringish_optional_key?: WithDefault, + stringish_optional_both?: WithDefault, + + // Stringish props, null default + stringish_null_optional_key?: WithDefault, + stringish_null_optional_both?: WithDefault, + + // Double props + double_required: Double, + double_optional_key?: WithDefault, + double_optional_both?: WithDefault, + + // Float props + float_required: Float, + float_optional_key?: WithDefault, + float_optional_both?: WithDefault, + + // Float props, null default + float_null_optional_key?: WithDefault, + float_null_optional_both?: WithDefault, + + // Int32 props + int32_required: Int32, + int32_optional_key?: WithDefault, + int32_optional_both?: WithDefault, + + // String enum props + enum_optional_key?: WithDefault<'small' | 'large', 'small'>, + enum_optional_both?: WithDefault<'small' | 'large', 'small'>, + + // Int enum props + int_enum_optional_key?: WithDefault<0 | 1, 0>, + + // Object props + object_optional_key?: $ReadOnly<{| prop: string |}>, + object_optional_both?: ?$ReadOnly<{| prop: string |}>, + object_optional_value: ?$ReadOnly<{| prop: string |}>, + + // ImageSource props + image_required: ImageSource, + image_optional_value: ?ImageSource, + image_optional_both?: ?ImageSource, + + // ColorValue props + color_required: ColorValue, + color_optional_key?: ColorValue, + color_optional_value: ?ColorValue, + color_optional_both?: ?ColorValue, + + // ColorArrayValue props + color_array_required: ColorArrayValue, + color_array_optional_key?: ColorArrayValue, + color_array_optional_value: ?ColorArrayValue, + color_array_optional_both?: ?ColorArrayValue, + + // ProcessedColorValue props + processed_color_required: ProcessedColorValue, + processed_color_optional_key?: ProcessedColorValue, + processed_color_optional_value: ?ProcessedColorValue, + processed_color_optional_both?: ?ProcessedColorValue, + + // PointValue props + point_required: PointValue, + point_optional_key?: PointValue, + point_optional_value: ?PointValue, + point_optional_both?: ?PointValue, + + // EdgeInsets props + insets_required: EdgeInsetsValue, + insets_optional_key?: EdgeInsetsValue, + insets_optional_value: ?EdgeInsetsValue, + insets_optional_both?: ?EdgeInsetsValue, + + // DimensionValue props + dimension_required: DimensionValue, + dimension_optional_key?: DimensionValue, + dimension_optional_value: ?DimensionValue, + dimension_optional_both?: ?DimensionValue, + + // Mixed props + mixed_required: UnsafeMixed, + mixed_optional_key?: UnsafeMixed, +|}>; + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; +const ARRAY_PROP_TYPES_NO_EVENTS = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {Int32, Double, Float, WithDefault} from 'CodegenTypes'; +import type {ImageSource} from 'ImageSource'; +import type {ColorValue, PointValue, ProcessColorValue, EdgeInsetsValue, DimensionValue} from 'StyleSheetTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +type ObjectType = $ReadOnly<{| prop: string |}>; +type ArrayObjectType = $ReadOnlyArray<$ReadOnly<{| prop: string |}>>; + +type ModuleProps = $ReadOnly<{| + ...ViewProps, + + // Props + // Boolean props + array_boolean_required: $ReadOnlyArray, + array_boolean_optional_key?: $ReadOnlyArray, + array_boolean_optional_value: ?$ReadOnlyArray, + array_boolean_optional_both?: ?$ReadOnlyArray, + + // String props + array_string_required: $ReadOnlyArray, + array_string_optional_key?: $ReadOnlyArray, + array_string_optional_value: ?$ReadOnlyArray, + array_string_optional_both?: ?$ReadOnlyArray, + + // Double props + array_double_required: $ReadOnlyArray, + array_double_optional_key?: $ReadOnlyArray, + array_double_optional_value: ?$ReadOnlyArray, + array_double_optional_both?: ?$ReadOnlyArray, + + // Float props + array_float_required: $ReadOnlyArray, + array_float_optional_key?: $ReadOnlyArray, + array_float_optional_value: ?$ReadOnlyArray, + array_float_optional_both?: ?$ReadOnlyArray, + + // Int32 props + array_int32_required: $ReadOnlyArray, + array_int32_optional_key?: $ReadOnlyArray, + array_int32_optional_value: ?$ReadOnlyArray, + array_int32_optional_both?: ?$ReadOnlyArray, + + // String enum props + array_enum_optional_key?: WithDefault< + $ReadOnlyArray<'small' | 'large'>, + 'small', + >, + array_enum_optional_both?: WithDefault< + $ReadOnlyArray<'small' | 'large'>, + 'small', + >, + + // ImageSource props + array_image_required: $ReadOnlyArray, + array_image_optional_key?: $ReadOnlyArray, + array_image_optional_value: ?$ReadOnlyArray, + array_image_optional_both?: ?$ReadOnlyArray, + + // ColorValue props + array_color_required: $ReadOnlyArray, + array_color_optional_key?: $ReadOnlyArray, + array_color_optional_value: ?$ReadOnlyArray, + array_color_optional_both?: ?$ReadOnlyArray, + + // PointValue props + array_point_required: $ReadOnlyArray, + array_point_optional_key?: $ReadOnlyArray, + array_point_optional_value: ?$ReadOnlyArray, + array_point_optional_both?: ?$ReadOnlyArray, + + // EdgeInsetsValue props + array_insets_required: $ReadOnlyArray, + array_insets_optional_key?: $ReadOnlyArray, + array_insets_optional_value: ?$ReadOnlyArray, + array_insets_optional_both?: ?$ReadOnlyArray, + + // DimensionValue props + array_dimension_required: $ReadOnlyArray, + array_dimension_optional_key?: $ReadOnlyArray, + array_dimension_optional_value: ?$ReadOnlyArray, + array_dimension_optional_both?: ?$ReadOnlyArray, + + // Object props + array_object_required: $ReadOnlyArray<$ReadOnly<{| prop: string |}>>, + array_object_optional_key?: $ReadOnlyArray<$ReadOnly<{| prop: string |}>>, + array_object_optional_value: ?ArrayObjectType, + array_object_optional_both?: ?$ReadOnlyArray, + + // Nested array object types + array_of_array_object_required: $ReadOnlyArray< + $ReadOnly<{| + // This needs to be the same name as the top level array above + array_object_required: $ReadOnlyArray<$ReadOnly<{| prop: string |}>>, + |}> + >, + array_of_array_object_optional_key?: $ReadOnlyArray< + $ReadOnly<{| + // This needs to be the same name as the top level array above + array_object_optional_key: $ReadOnlyArray<$ReadOnly<{| prop?: string |}>>, + |}> + >, + array_of_array_object_optional_value: ?$ReadOnlyArray< + $ReadOnly<{| + // This needs to be the same name as the top level array above + array_object_optional_value: $ReadOnlyArray<$ReadOnly<{| prop: ?string |}>>, + |}> + >, + array_of_array_object_optional_both?: ?$ReadOnlyArray< + $ReadOnly<{| + // This needs to be the same name as the top level array above + array_object_optional_both: $ReadOnlyArray<$ReadOnly<{| prop?: ?string |}>>, + |}> + >, + + // Nested array of array of object types + array_of_array_of_object_required: $ReadOnlyArray< + $ReadOnlyArray< + $ReadOnly<{| + prop: string, + |}>, + >, + >, + + // Nested array of array of object types (in file) + array_of_array_of_object_required_in_file: $ReadOnlyArray< + $ReadOnlyArray, + >, + + // Nested array of array of object types (with spread) + array_of_array_of_object_required_with_spread: $ReadOnlyArray< + $ReadOnlyArray< + $ReadOnly<{| + ...ObjectType + |}>, + >, + >, +|}>; + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; +const OBJECT_PROP_TYPES_NO_EVENTS = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {Int32, Double, Float, WithDefault} from 'CodegenTypes'; +import type {ImageSource} from 'ImageSource'; +import type {ColorValue, PointValue, EdgeInsetsValue} from 'StyleSheetTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +type ModuleProps = $ReadOnly<{| + ...ViewProps, + + // Props + // Boolean props + boolean_required: $ReadOnly<{|prop: boolean|}>, + boolean_optional: $ReadOnly<{|prop?: WithDefault|}>, + + // String props + string_required: $ReadOnly<{|prop: string|}>, + string_optional: $ReadOnly<{|prop?: WithDefault|}>, + + // Double props + double_required: $ReadOnly<{|prop: Double|}>, + double_optional: $ReadOnly<{|prop?: WithDefault|}>, + + // Float props + float_required: $ReadOnly<{|prop: Float|}>, + float_optional: $ReadOnly<{|prop?: WithDefault|}>, + + // Int32 props + int_required: $ReadOnly<{|prop: Int32|}>, + int_optional: $ReadOnly<{|prop?: WithDefault|}>, + + // String enum props + enum_optional: $ReadOnly<{| + prop?: WithDefault<$ReadOnlyArray<'small' | 'large'>, 'small'>, + |}>, + + // ImageSource props + image_required: $ReadOnly<{|prop: ImageSource|}>, + image_optional_key: $ReadOnly<{|prop?: ImageSource|}>, + image_optional_value: $ReadOnly<{|prop: ?ImageSource|}>, + image_optional_both: $ReadOnly<{|prop?: ?ImageSource|}>, + + // ColorValue props + color_required: $ReadOnly<{|prop: ColorValue|}>, + color_optional_key: $ReadOnly<{|prop?: ColorValue|}>, + color_optional_value: $ReadOnly<{|prop: ?ColorValue|}>, + color_optional_both: $ReadOnly<{|prop?: ?ColorValue|}>, + + // ProcessedColorValue props + processed_color_required: $ReadOnly<{|prop: ProcessedColorValue|}>, + processed_color_optional_key: $ReadOnly<{|prop?: ProcessedColorValue|}>, + processed_color_optional_value: $ReadOnly<{|prop: ?ProcessedColorValue|}>, + processed_color_optional_both: $ReadOnly<{|prop?: ?ProcessedColorValue|}>, + + // PointValue props + point_required: $ReadOnly<{|prop: PointValue|}>, + point_optional_key: $ReadOnly<{|prop?: PointValue|}>, + point_optional_value: $ReadOnly<{|prop: ?PointValue|}>, + point_optional_both: $ReadOnly<{|prop?: ?PointValue|}>, + + // EdgeInsetsValue props + insets_required: $ReadOnly<{|prop: EdgeInsetsValue|}>, + insets_optional_key: $ReadOnly<{|prop?: EdgeInsetsValue|}>, + insets_optional_value: $ReadOnly<{|prop: ?EdgeInsetsValue|}>, + insets_optional_both: $ReadOnly<{|prop?: ?EdgeInsetsValue|}>, + + // DimensionValue props + dimension_required: $ReadOnly<{|prop: DimensionValue|}>, + dimension_optional_key: $ReadOnly<{|prop?: DimensionValue|}>, + dimension_optional_value: $ReadOnly<{|prop: ?DimensionValue|}>, + dimension_optional_both: $ReadOnly<{|prop?: ?DimensionValue|}>, + + // Nested object props + object_required: $ReadOnly<{|prop: $ReadOnly<{nestedProp: string}>|}>, + object_optional_key?: $ReadOnly<{|prop: $ReadOnly<{nestedProp: string}>|}>, + object_optional_value: ?$ReadOnly<{|prop: $ReadOnly<{nestedProp: string}>|}>, + object_optional_both?: ?$ReadOnly<{|prop: $ReadOnly<{nestedProp: string}>|}>, +|}>; + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; +const PROPS_ALIASED_LOCALLY = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +type DeepSpread = $ReadOnly<{| + otherStringProp: string, +|}>; + +export type PropsInFile = $ReadOnly<{| + ...DeepSpread, + isEnabled: boolean, + label: string, +|}>; + +export type ModuleProps = $ReadOnly<{| + ...ViewProps, + + ...PropsInFile, + + localType: $ReadOnly<{| + ...PropsInFile + |}>, + + localArr: $ReadOnlyArray +|}>; + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; +const EVENTS_DEFINED_INLINE_WITH_ALL_TYPES = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +import type {HostComponent} from 'react-native'; +const codegenNativeComponent = require('codegenNativeComponent'); + +import type { + Int32, + Double, + Float, + BubblingEventHandler, + DirectEventHandler, +} from 'CodegenTypes'; + +import type {ViewProps} from 'ViewPropTypes'; + +type ModuleProps = $ReadOnly<{| + ...ViewProps, + // No Props + + // Events + onDirectEventDefinedInline: + DirectEventHandler< + $ReadOnly<{| + ${EVENT_DEFINITION} + |}>, + >, + + onDirectEventDefinedInlineOptionalKey?: + DirectEventHandler< + $ReadOnly<{| + ${EVENT_DEFINITION} + |}>, + >, + + onDirectEventDefinedInlineOptionalValue: ? + DirectEventHandler< + $ReadOnly<{| + ${EVENT_DEFINITION} + |}>, + >, + + onDirectEventDefinedInlineOptionalBoth?: ? + DirectEventHandler< + $ReadOnly<{| + ${EVENT_DEFINITION} + |}>, + >, + + onDirectEventDefinedInlineWithPaperName?: ? + DirectEventHandler< + $ReadOnly<{| + ${EVENT_DEFINITION} + |}>, + 'paperDirectEventDefinedInlineWithPaperName', + >, + + onBubblingEventDefinedInline: + BubblingEventHandler< + $ReadOnly<{| + ${EVENT_DEFINITION} + |}>, + >, + + onBubblingEventDefinedInlineOptionalKey?: + BubblingEventHandler< + $ReadOnly<{| + ${EVENT_DEFINITION} + |}>, + >, + + onBubblingEventDefinedInlineOptionalValue: ? + BubblingEventHandler< + $ReadOnly<{| + ${EVENT_DEFINITION} + |}>, + >, + + onBubblingEventDefinedInlineOptionalBoth?: ? + BubblingEventHandler< + $ReadOnly<{| + ${EVENT_DEFINITION} + |}>, + >, + + onBubblingEventDefinedInlineWithPaperName?: ? + BubblingEventHandler< + $ReadOnly<{| + ${EVENT_DEFINITION} + |}>, + 'paperBubblingEventDefinedInlineWithPaperName' + >, +|}>; + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; +const EVENTS_DEFINED_AS_NULL_INLINE = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {BubblingEventHandler, DirectEventHandler} from 'CodegenTypese'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +type ModuleProps = $ReadOnly<{| + ...ViewProps, + + // No props + + // Events defined inline + onDirectEventDefinedInlineNull: DirectEventHandler, + onDirectEventDefinedInlineNullOptionalKey?: DirectEventHandler, + onDirectEventDefinedInlineNullOptionalValue: ?DirectEventHandler, + onDirectEventDefinedInlineNullOptionalBoth?: DirectEventHandler, + onDirectEventDefinedInlineNullWithPaperName?: ?DirectEventHandler< + null, + 'paperDirectEventDefinedInlineNullWithPaperName', + >, + + onBubblingEventDefinedInlineNull: BubblingEventHandler, + onBubblingEventDefinedInlineNullOptionalKey?: BubblingEventHandler, + onBubblingEventDefinedInlineNullOptionalValue: ?BubblingEventHandler, + onBubblingEventDefinedInlineNullOptionalBoth?: ?BubblingEventHandler, + onBubblingEventDefinedInlineNullWithPaperName?: ?BubblingEventHandler< + null, + 'paperBubblingEventDefinedInlineNullWithPaperName', + >, +|}>; + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; +const PROPS_AND_EVENTS_TYPES_EXPORTED = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +import type { + BubblingEventHandler, + DirectEventHandler, +} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +export type EventInFile = $ReadOnly<{| + ${EVENT_DEFINITION} +|}>; + +export type ModuleProps = $ReadOnly<{| + ...ViewProps, + + // No props + + // Events defined inline + onBubblingEventDefinedInline: BubblingEventHandler, + onBubblingEventDefinedInlineWithPaperName: BubblingEventHandler, + onDirectEventDefinedInline: DirectEventHandler, + onDirectEventDefinedInlineWithPaperName: DirectEventHandler, +|}>; + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; +const PROPS_AS_EXTERNAL_TYPES = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {HostComponent} from 'react-native'; + +export type String = string; +export type AnotherArray = $ReadOnlyArray; + +export type ModuleProps = $ReadOnly<{| + disable: String, + array: AnotherArray, +|}>; + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; +const COMMANDS_DEFINED_WITH_ALL_TYPES = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +const codegenNativeCommands = require('codegenNativeCommands'); +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {Int32, Double, Float} from 'CodegenTypes'; +import type {RootTag} from 'RCTExport'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + + +export type ModuleProps = $ReadOnly<{| + ...ViewProps, + // No props or events +|}>; + +type NativeType = HostComponent; + +interface NativeCommands { + +handleRootTag: (viewRef: React.ElementRef, rootTag: RootTag) => void; + +hotspotUpdate: (viewRef: React.ElementRef, x: Int32, y: Int32) => void; + scrollTo( + viewRef: React.ElementRef, + x: Float, + y: Int32, + z: Double, + animated: boolean, + ): void; +} + +export const Commands = codegenNativeCommands({ + supportedCommands: ['handleRootTag', 'hotspotUpdate', 'scrollTo'], +}); + +export default (codegenNativeComponent( + 'Module', +): NativeType); +`; +const COMMANDS_WITH_EXTERNAL_TYPES = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +const codegenNativeCommands = require('codegenNativeCommands'); +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {Int32} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +export type Boolean = boolean; +export type Int = Int32; +export type Void = void; + +export type ModuleProps = $ReadOnly<{| + ...ViewProps, + // No props or events +|}>; + +type NativeType = HostComponent; + +export type ScrollTo = ( + viewRef: React.ElementRef, + y: Int, + animated: Boolean, +) => Void; + +interface NativeCommands { + +scrollTo: ScrollTo; + +addOverlays: ( + viewRef: React.ElementRef, + overlayColorsReadOnly: $ReadOnlyArray, + overlayColorsArray: Array, + overlayColorsArrayAnnotation: string[], + ) => void; +} + +export const Commands = codegenNativeCommands({ + supportedCommands: ['scrollTo', 'addOverlays'], +}); + +export default (codegenNativeComponent( + 'Module', +): NativeType); +`; +const COMMANDS_EVENTS_TYPES_EXPORTED = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +import type { + BubblingEventHandler, + DirectEventHandler, +} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +export type EventInFile = $ReadOnly<{| + ${EVENT_DEFINITION} +|}>; + +export type Boolean = boolean; +export type Int = Int32; +export type Void = void; + +export type ModuleProps = $ReadOnly<{| + ...ViewProps, + + // No props + + // Events defined inline + onBubblingEventDefinedInline: BubblingEventHandler, + onBubblingEventDefinedInlineWithPaperName: BubblingEventHandler, + onDirectEventDefinedInline: DirectEventHandler, + onDirectEventDefinedInlineWithPaperName: DirectEventHandler, +|}>; + +type NativeType = HostComponent; + +export type ScrollTo = (viewRef: React.ElementRef, y: Int, animated: Boolean) => Void; + +interface NativeCommands { + +scrollTo: ScrollTo; +} + +export const Commands = codegenNativeCommands({ + supportedCommands: ['scrollTo'] +}); + +export default (codegenNativeComponent( + 'Module', +): NativeType); +`; +module.exports = { + ALL_PROP_TYPES_NO_EVENTS, + ARRAY_PROP_TYPES_NO_EVENTS, + OBJECT_PROP_TYPES_NO_EVENTS, + PROPS_ALIASED_LOCALLY, + ONE_OF_EACH_PROP_EVENT_DEFAULT_AND_OPTIONS, + ONE_OF_EACH_PROP_EVENT_DEFAULT_AND_OPTIONS_NO_CAST, + NO_PROPS_EVENTS_ONLY_DEPRECATED_VIEW_CONFIG_NAME_OPTION, + EVENTS_DEFINED_INLINE_WITH_ALL_TYPES, + EVENTS_DEFINED_AS_NULL_INLINE, + PROPS_AND_EVENTS_TYPES_EXPORTED, + COMMANDS_EVENTS_TYPES_EXPORTED, + COMMANDS_DEFINED_WITH_ALL_TYPES, + PROPS_AS_EXTERNAL_TYPES, + COMMANDS_WITH_EXTERNAL_TYPES, +}; diff --git a/packages/react-native-codegen/lib/parsers/flow/components/__test_fixtures__/fixtures.js.flow b/packages/react-native-codegen/lib/parsers/flow/components/__test_fixtures__/fixtures.js.flow new file mode 100644 index 00000000000000..7b93444feca642 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/flow/components/__test_fixtures__/fixtures.js.flow @@ -0,0 +1,1095 @@ +/** + * 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-local + * @format + */ + +// @licenselint-loose-mode + +'use strict'; + +const EVENT_DEFINITION = ` + boolean_required: boolean, + boolean_optional_key?: boolean, + boolean_optional_value: ?boolean, + boolean_optional_both?: ?boolean, + + string_required: string, + string_optional_key?: string, + string_optional_value: ?string, + string_optional_both?: ?string, + + double_required: Double, + double_optional_key?: Double, + double_optional_value: ?Double, + double_optional_both?: ?Double, + + float_required: Float, + float_optional_key?: Float, + float_optional_value: ?Float, + float_optional_both?: ?Float, + + int32_required: Int32, + int32_optional_key?: Int32, + int32_optional_value: ?Int32, + int32_optional_both?: ?Int32, + + enum_required: ('small' | 'large'), + enum_optional_key?: ('small' | 'large'), + enum_optional_value: ?('small' | 'large'), + enum_optional_both?: ?('small' | 'large'), + + object_required: { + boolean_required: boolean, + }, + + object_optional_key?: { + string_optional_key?: string, + }, + + object_optional_value: ?{ + float_optional_value: ?Float, + }, + + object_optional_both?: ?{ + int32_optional_both?: ?Int32, + }, + + object_required_nested_2_layers: { + object_optional_nested_1_layer?: ?{ + boolean_required: Int32, + string_optional_key?: string, + double_optional_value: ?Double, + float_optional_value: ?Float, + int32_optional_both?: ?Int32, + } + }, + + object_readonly_required: $ReadOnly<{ + boolean_required: boolean, + }>, + + object_readonly_optional_key?: $ReadOnly<{ + string_optional_key?: string, + }>, + + object_readonly_optional_value: ?$ReadOnly<{ + float_optional_value: ?Float, + }>, + + object_readonly_optional_both?: ?$ReadOnly<{ + int32_optional_both?: ?Int32, + }>, + + boolean_array_required: $ReadOnlyArray, + boolean_array_optional_key?: boolean[], + boolean_array_optional_value: ?$ReadOnlyArray, + boolean_array_optional_both?: ?boolean[], + + string_array_required: $ReadOnlyArray, + string_array_optional_key?: string[], + string_array_optional_value: ?$ReadOnlyArray, + string_array_optional_both?: ?string[], + + double_array_required: $ReadOnlyArray, + double_array_optional_key?: Double[], + double_array_optional_value: ?$ReadOnlyArray, + double_array_optional_both?: ?Double[], + + float_array_required: $ReadOnlyArray, + float_array_optional_key?: Float[], + float_array_optional_value: ?$ReadOnlyArray, + float_array_optional_both?: ?Float[], + + int32_array_required: $ReadOnlyArray, + int32_array_optional_key?: Int32[], + int32_array_optional_value: ?$ReadOnlyArray, + int32_array_optional_both?: ?Int32[], + + enum_array_required: $ReadOnlyArray<('small' | 'large')>, + enum_array_optional_key?: ('small' | 'large')[], + enum_array_optional_value: ?$ReadOnlyArray<('small' | 'large')>, + enum_array_optional_both?: ?('small' | 'large')[], + + object_array_required: $ReadOnlyArray<{ + boolean_required: boolean, + }>, + + object_array_optional_key?: { + string_optional_key?: string, + }[], + + object_array_optional_value: ?$ReadOnlyArray<{ + float_optional_value: ?Float, + }>, + + object_array_optional_both?: ?{ + int32_optional_both?: ?Int32, + }[], + + int32_array_array_required: $ReadOnlyArray<$ReadOnlyArray>, + int32_array_array_optional_key?: Int32[][], + int32_array_array_optional_value: ?$ReadOnlyArray<$ReadOnlyArray>, + int32_array_array_optional_both?: ?Int32[][], +`; + +const ONE_OF_EACH_PROP_EVENT_DEFAULT_AND_OPTIONS = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type { + BubblingEventHandler, + DirectEventHandler, + WithDefault, +} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +type ModuleProps = $ReadOnly<{| + ...ViewProps, + + // Props + boolean_default_true_optional_both?: WithDefault, + + // Events + onDirectEventDefinedInlineNull: DirectEventHandler, + onBubblingEventDefinedInlineNull: BubblingEventHandler, +|}>; + +export default (codegenNativeComponent('Module', { + interfaceOnly: true, + paperComponentName: 'RCTModule', +}): HostComponent); +`; + +const ONE_OF_EACH_PROP_EVENT_DEFAULT_AND_OPTIONS_NO_CAST = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type { + BubblingEventHandler, + DirectEventHandler, + WithDefault, +} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +type ModuleProps = $ReadOnly<{| + ...ViewProps, + + // Props + boolean_default_true_optional_both?: WithDefault, + + // Events + onDirectEventDefinedInlineNull: DirectEventHandler, + onBubblingEventDefinedInlineNull: BubblingEventHandler, +|}>; + +export default codegenNativeComponent('Module', { + interfaceOnly: true, + excludedPlatforms: ['android'], + paperComponentName: 'RCTModule', +}); +`; + +const NO_PROPS_EVENTS_ONLY_DEPRECATED_VIEW_CONFIG_NAME_OPTION = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +type ModuleProps = $ReadOnly<{| + ...ViewProps, +|}>; + +export default (codegenNativeComponent('Module', { + deprecatedViewConfigName: 'DeprecateModuleName', +}): HostComponent); +`; + +const ALL_PROP_TYPES_NO_EVENTS = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {Int32, Double, Float, WithDefault, UnsafeMixed} from 'CodegenTypes'; +import type {ImageSource} from 'ImageSource'; +import type {ColorValue, ColorArrayValue, PointValue, EdgeInsetsValue, DimensionValue} from 'StyleSheetTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +type ModuleProps = $ReadOnly<{| + ...ViewProps, + + // Props + // Boolean props + boolean_required: boolean, + boolean_optional_key?: WithDefault, + boolean_optional_both?: WithDefault, + + // Boolean props, null default + boolean_null_optional_key?: WithDefault, + boolean_null_optional_both?: WithDefault, + + // String props + string_required: string, + string_optional_key?: WithDefault, + string_optional_both?: WithDefault, + + // String props, null default + string_null_optional_key?: WithDefault, + string_null_optional_both?: WithDefault, + + // Stringish props + stringish_required: Stringish, + stringish_optional_key?: WithDefault, + stringish_optional_both?: WithDefault, + + // Stringish props, null default + stringish_null_optional_key?: WithDefault, + stringish_null_optional_both?: WithDefault, + + // Double props + double_required: Double, + double_optional_key?: WithDefault, + double_optional_both?: WithDefault, + + // Float props + float_required: Float, + float_optional_key?: WithDefault, + float_optional_both?: WithDefault, + + // Float props, null default + float_null_optional_key?: WithDefault, + float_null_optional_both?: WithDefault, + + // Int32 props + int32_required: Int32, + int32_optional_key?: WithDefault, + int32_optional_both?: WithDefault, + + // String enum props + enum_optional_key?: WithDefault<'small' | 'large', 'small'>, + enum_optional_both?: WithDefault<'small' | 'large', 'small'>, + + // Int enum props + int_enum_optional_key?: WithDefault<0 | 1, 0>, + + // Object props + object_optional_key?: $ReadOnly<{| prop: string |}>, + object_optional_both?: ?$ReadOnly<{| prop: string |}>, + object_optional_value: ?$ReadOnly<{| prop: string |}>, + + // ImageSource props + image_required: ImageSource, + image_optional_value: ?ImageSource, + image_optional_both?: ?ImageSource, + + // ColorValue props + color_required: ColorValue, + color_optional_key?: ColorValue, + color_optional_value: ?ColorValue, + color_optional_both?: ?ColorValue, + + // ColorArrayValue props + color_array_required: ColorArrayValue, + color_array_optional_key?: ColorArrayValue, + color_array_optional_value: ?ColorArrayValue, + color_array_optional_both?: ?ColorArrayValue, + + // ProcessedColorValue props + processed_color_required: ProcessedColorValue, + processed_color_optional_key?: ProcessedColorValue, + processed_color_optional_value: ?ProcessedColorValue, + processed_color_optional_both?: ?ProcessedColorValue, + + // PointValue props + point_required: PointValue, + point_optional_key?: PointValue, + point_optional_value: ?PointValue, + point_optional_both?: ?PointValue, + + // EdgeInsets props + insets_required: EdgeInsetsValue, + insets_optional_key?: EdgeInsetsValue, + insets_optional_value: ?EdgeInsetsValue, + insets_optional_both?: ?EdgeInsetsValue, + + // DimensionValue props + dimension_required: DimensionValue, + dimension_optional_key?: DimensionValue, + dimension_optional_value: ?DimensionValue, + dimension_optional_both?: ?DimensionValue, + + // Mixed props + mixed_required: UnsafeMixed, + mixed_optional_key?: UnsafeMixed, +|}>; + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; + +const ARRAY_PROP_TYPES_NO_EVENTS = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {Int32, Double, Float, WithDefault} from 'CodegenTypes'; +import type {ImageSource} from 'ImageSource'; +import type {ColorValue, PointValue, ProcessColorValue, EdgeInsetsValue, DimensionValue} from 'StyleSheetTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +type ObjectType = $ReadOnly<{| prop: string |}>; +type ArrayObjectType = $ReadOnlyArray<$ReadOnly<{| prop: string |}>>; + +type ModuleProps = $ReadOnly<{| + ...ViewProps, + + // Props + // Boolean props + array_boolean_required: $ReadOnlyArray, + array_boolean_optional_key?: $ReadOnlyArray, + array_boolean_optional_value: ?$ReadOnlyArray, + array_boolean_optional_both?: ?$ReadOnlyArray, + + // String props + array_string_required: $ReadOnlyArray, + array_string_optional_key?: $ReadOnlyArray, + array_string_optional_value: ?$ReadOnlyArray, + array_string_optional_both?: ?$ReadOnlyArray, + + // Double props + array_double_required: $ReadOnlyArray, + array_double_optional_key?: $ReadOnlyArray, + array_double_optional_value: ?$ReadOnlyArray, + array_double_optional_both?: ?$ReadOnlyArray, + + // Float props + array_float_required: $ReadOnlyArray, + array_float_optional_key?: $ReadOnlyArray, + array_float_optional_value: ?$ReadOnlyArray, + array_float_optional_both?: ?$ReadOnlyArray, + + // Int32 props + array_int32_required: $ReadOnlyArray, + array_int32_optional_key?: $ReadOnlyArray, + array_int32_optional_value: ?$ReadOnlyArray, + array_int32_optional_both?: ?$ReadOnlyArray, + + // String enum props + array_enum_optional_key?: WithDefault< + $ReadOnlyArray<'small' | 'large'>, + 'small', + >, + array_enum_optional_both?: WithDefault< + $ReadOnlyArray<'small' | 'large'>, + 'small', + >, + + // ImageSource props + array_image_required: $ReadOnlyArray, + array_image_optional_key?: $ReadOnlyArray, + array_image_optional_value: ?$ReadOnlyArray, + array_image_optional_both?: ?$ReadOnlyArray, + + // ColorValue props + array_color_required: $ReadOnlyArray, + array_color_optional_key?: $ReadOnlyArray, + array_color_optional_value: ?$ReadOnlyArray, + array_color_optional_both?: ?$ReadOnlyArray, + + // PointValue props + array_point_required: $ReadOnlyArray, + array_point_optional_key?: $ReadOnlyArray, + array_point_optional_value: ?$ReadOnlyArray, + array_point_optional_both?: ?$ReadOnlyArray, + + // EdgeInsetsValue props + array_insets_required: $ReadOnlyArray, + array_insets_optional_key?: $ReadOnlyArray, + array_insets_optional_value: ?$ReadOnlyArray, + array_insets_optional_both?: ?$ReadOnlyArray, + + // DimensionValue props + array_dimension_required: $ReadOnlyArray, + array_dimension_optional_key?: $ReadOnlyArray, + array_dimension_optional_value: ?$ReadOnlyArray, + array_dimension_optional_both?: ?$ReadOnlyArray, + + // Object props + array_object_required: $ReadOnlyArray<$ReadOnly<{| prop: string |}>>, + array_object_optional_key?: $ReadOnlyArray<$ReadOnly<{| prop: string |}>>, + array_object_optional_value: ?ArrayObjectType, + array_object_optional_both?: ?$ReadOnlyArray, + + // Nested array object types + array_of_array_object_required: $ReadOnlyArray< + $ReadOnly<{| + // This needs to be the same name as the top level array above + array_object_required: $ReadOnlyArray<$ReadOnly<{| prop: string |}>>, + |}> + >, + array_of_array_object_optional_key?: $ReadOnlyArray< + $ReadOnly<{| + // This needs to be the same name as the top level array above + array_object_optional_key: $ReadOnlyArray<$ReadOnly<{| prop?: string |}>>, + |}> + >, + array_of_array_object_optional_value: ?$ReadOnlyArray< + $ReadOnly<{| + // This needs to be the same name as the top level array above + array_object_optional_value: $ReadOnlyArray<$ReadOnly<{| prop: ?string |}>>, + |}> + >, + array_of_array_object_optional_both?: ?$ReadOnlyArray< + $ReadOnly<{| + // This needs to be the same name as the top level array above + array_object_optional_both: $ReadOnlyArray<$ReadOnly<{| prop?: ?string |}>>, + |}> + >, + + // Nested array of array of object types + array_of_array_of_object_required: $ReadOnlyArray< + $ReadOnlyArray< + $ReadOnly<{| + prop: string, + |}>, + >, + >, + + // Nested array of array of object types (in file) + array_of_array_of_object_required_in_file: $ReadOnlyArray< + $ReadOnlyArray, + >, + + // Nested array of array of object types (with spread) + array_of_array_of_object_required_with_spread: $ReadOnlyArray< + $ReadOnlyArray< + $ReadOnly<{| + ...ObjectType + |}>, + >, + >, +|}>; + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; + +const OBJECT_PROP_TYPES_NO_EVENTS = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {Int32, Double, Float, WithDefault} from 'CodegenTypes'; +import type {ImageSource} from 'ImageSource'; +import type {ColorValue, PointValue, EdgeInsetsValue} from 'StyleSheetTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +type ModuleProps = $ReadOnly<{| + ...ViewProps, + + // Props + // Boolean props + boolean_required: $ReadOnly<{|prop: boolean|}>, + boolean_optional: $ReadOnly<{|prop?: WithDefault|}>, + + // String props + string_required: $ReadOnly<{|prop: string|}>, + string_optional: $ReadOnly<{|prop?: WithDefault|}>, + + // Double props + double_required: $ReadOnly<{|prop: Double|}>, + double_optional: $ReadOnly<{|prop?: WithDefault|}>, + + // Float props + float_required: $ReadOnly<{|prop: Float|}>, + float_optional: $ReadOnly<{|prop?: WithDefault|}>, + + // Int32 props + int_required: $ReadOnly<{|prop: Int32|}>, + int_optional: $ReadOnly<{|prop?: WithDefault|}>, + + // String enum props + enum_optional: $ReadOnly<{| + prop?: WithDefault<$ReadOnlyArray<'small' | 'large'>, 'small'>, + |}>, + + // ImageSource props + image_required: $ReadOnly<{|prop: ImageSource|}>, + image_optional_key: $ReadOnly<{|prop?: ImageSource|}>, + image_optional_value: $ReadOnly<{|prop: ?ImageSource|}>, + image_optional_both: $ReadOnly<{|prop?: ?ImageSource|}>, + + // ColorValue props + color_required: $ReadOnly<{|prop: ColorValue|}>, + color_optional_key: $ReadOnly<{|prop?: ColorValue|}>, + color_optional_value: $ReadOnly<{|prop: ?ColorValue|}>, + color_optional_both: $ReadOnly<{|prop?: ?ColorValue|}>, + + // ProcessedColorValue props + processed_color_required: $ReadOnly<{|prop: ProcessedColorValue|}>, + processed_color_optional_key: $ReadOnly<{|prop?: ProcessedColorValue|}>, + processed_color_optional_value: $ReadOnly<{|prop: ?ProcessedColorValue|}>, + processed_color_optional_both: $ReadOnly<{|prop?: ?ProcessedColorValue|}>, + + // PointValue props + point_required: $ReadOnly<{|prop: PointValue|}>, + point_optional_key: $ReadOnly<{|prop?: PointValue|}>, + point_optional_value: $ReadOnly<{|prop: ?PointValue|}>, + point_optional_both: $ReadOnly<{|prop?: ?PointValue|}>, + + // EdgeInsetsValue props + insets_required: $ReadOnly<{|prop: EdgeInsetsValue|}>, + insets_optional_key: $ReadOnly<{|prop?: EdgeInsetsValue|}>, + insets_optional_value: $ReadOnly<{|prop: ?EdgeInsetsValue|}>, + insets_optional_both: $ReadOnly<{|prop?: ?EdgeInsetsValue|}>, + + // DimensionValue props + dimension_required: $ReadOnly<{|prop: DimensionValue|}>, + dimension_optional_key: $ReadOnly<{|prop?: DimensionValue|}>, + dimension_optional_value: $ReadOnly<{|prop: ?DimensionValue|}>, + dimension_optional_both: $ReadOnly<{|prop?: ?DimensionValue|}>, + + // Nested object props + object_required: $ReadOnly<{|prop: $ReadOnly<{nestedProp: string}>|}>, + object_optional_key?: $ReadOnly<{|prop: $ReadOnly<{nestedProp: string}>|}>, + object_optional_value: ?$ReadOnly<{|prop: $ReadOnly<{nestedProp: string}>|}>, + object_optional_both?: ?$ReadOnly<{|prop: $ReadOnly<{nestedProp: string}>|}>, +|}>; + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; + +const PROPS_ALIASED_LOCALLY = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +type DeepSpread = $ReadOnly<{| + otherStringProp: string, +|}>; + +export type PropsInFile = $ReadOnly<{| + ...DeepSpread, + isEnabled: boolean, + label: string, +|}>; + +export type ModuleProps = $ReadOnly<{| + ...ViewProps, + + ...PropsInFile, + + localType: $ReadOnly<{| + ...PropsInFile + |}>, + + localArr: $ReadOnlyArray +|}>; + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; + +const EVENTS_DEFINED_INLINE_WITH_ALL_TYPES = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +import type {HostComponent} from 'react-native'; +const codegenNativeComponent = require('codegenNativeComponent'); + +import type { + Int32, + Double, + Float, + BubblingEventHandler, + DirectEventHandler, +} from 'CodegenTypes'; + +import type {ViewProps} from 'ViewPropTypes'; + +type ModuleProps = $ReadOnly<{| + ...ViewProps, + // No Props + + // Events + onDirectEventDefinedInline: + DirectEventHandler< + $ReadOnly<{| + ${EVENT_DEFINITION} + |}>, + >, + + onDirectEventDefinedInlineOptionalKey?: + DirectEventHandler< + $ReadOnly<{| + ${EVENT_DEFINITION} + |}>, + >, + + onDirectEventDefinedInlineOptionalValue: ? + DirectEventHandler< + $ReadOnly<{| + ${EVENT_DEFINITION} + |}>, + >, + + onDirectEventDefinedInlineOptionalBoth?: ? + DirectEventHandler< + $ReadOnly<{| + ${EVENT_DEFINITION} + |}>, + >, + + onDirectEventDefinedInlineWithPaperName?: ? + DirectEventHandler< + $ReadOnly<{| + ${EVENT_DEFINITION} + |}>, + 'paperDirectEventDefinedInlineWithPaperName', + >, + + onBubblingEventDefinedInline: + BubblingEventHandler< + $ReadOnly<{| + ${EVENT_DEFINITION} + |}>, + >, + + onBubblingEventDefinedInlineOptionalKey?: + BubblingEventHandler< + $ReadOnly<{| + ${EVENT_DEFINITION} + |}>, + >, + + onBubblingEventDefinedInlineOptionalValue: ? + BubblingEventHandler< + $ReadOnly<{| + ${EVENT_DEFINITION} + |}>, + >, + + onBubblingEventDefinedInlineOptionalBoth?: ? + BubblingEventHandler< + $ReadOnly<{| + ${EVENT_DEFINITION} + |}>, + >, + + onBubblingEventDefinedInlineWithPaperName?: ? + BubblingEventHandler< + $ReadOnly<{| + ${EVENT_DEFINITION} + |}>, + 'paperBubblingEventDefinedInlineWithPaperName' + >, +|}>; + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; + +const EVENTS_DEFINED_AS_NULL_INLINE = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {BubblingEventHandler, DirectEventHandler} from 'CodegenTypese'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +type ModuleProps = $ReadOnly<{| + ...ViewProps, + + // No props + + // Events defined inline + onDirectEventDefinedInlineNull: DirectEventHandler, + onDirectEventDefinedInlineNullOptionalKey?: DirectEventHandler, + onDirectEventDefinedInlineNullOptionalValue: ?DirectEventHandler, + onDirectEventDefinedInlineNullOptionalBoth?: DirectEventHandler, + onDirectEventDefinedInlineNullWithPaperName?: ?DirectEventHandler< + null, + 'paperDirectEventDefinedInlineNullWithPaperName', + >, + + onBubblingEventDefinedInlineNull: BubblingEventHandler, + onBubblingEventDefinedInlineNullOptionalKey?: BubblingEventHandler, + onBubblingEventDefinedInlineNullOptionalValue: ?BubblingEventHandler, + onBubblingEventDefinedInlineNullOptionalBoth?: ?BubblingEventHandler, + onBubblingEventDefinedInlineNullWithPaperName?: ?BubblingEventHandler< + null, + 'paperBubblingEventDefinedInlineNullWithPaperName', + >, +|}>; + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; + +const PROPS_AND_EVENTS_TYPES_EXPORTED = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +import type { + BubblingEventHandler, + DirectEventHandler, +} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +export type EventInFile = $ReadOnly<{| + ${EVENT_DEFINITION} +|}>; + +export type ModuleProps = $ReadOnly<{| + ...ViewProps, + + // No props + + // Events defined inline + onBubblingEventDefinedInline: BubblingEventHandler, + onBubblingEventDefinedInlineWithPaperName: BubblingEventHandler, + onDirectEventDefinedInline: DirectEventHandler, + onDirectEventDefinedInlineWithPaperName: DirectEventHandler, +|}>; + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; + +const PROPS_AS_EXTERNAL_TYPES = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {HostComponent} from 'react-native'; + +export type String = string; +export type AnotherArray = $ReadOnlyArray; + +export type ModuleProps = $ReadOnly<{| + disable: String, + array: AnotherArray, +|}>; + +export default (codegenNativeComponent( + 'Module', +): HostComponent); +`; + +const COMMANDS_DEFINED_WITH_ALL_TYPES = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +const codegenNativeCommands = require('codegenNativeCommands'); +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {Int32, Double, Float} from 'CodegenTypes'; +import type {RootTag} from 'RCTExport'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + + +export type ModuleProps = $ReadOnly<{| + ...ViewProps, + // No props or events +|}>; + +type NativeType = HostComponent; + +interface NativeCommands { + +handleRootTag: (viewRef: React.ElementRef, rootTag: RootTag) => void; + +hotspotUpdate: (viewRef: React.ElementRef, x: Int32, y: Int32) => void; + scrollTo( + viewRef: React.ElementRef, + x: Float, + y: Int32, + z: Double, + animated: boolean, + ): void; +} + +export const Commands = codegenNativeCommands({ + supportedCommands: ['handleRootTag', 'hotspotUpdate', 'scrollTo'], +}); + +export default (codegenNativeComponent( + 'Module', +): NativeType); +`; + +const COMMANDS_WITH_EXTERNAL_TYPES = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +const codegenNativeCommands = require('codegenNativeCommands'); +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {Int32} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +export type Boolean = boolean; +export type Int = Int32; +export type Void = void; + +export type ModuleProps = $ReadOnly<{| + ...ViewProps, + // No props or events +|}>; + +type NativeType = HostComponent; + +export type ScrollTo = ( + viewRef: React.ElementRef, + y: Int, + animated: Boolean, +) => Void; + +interface NativeCommands { + +scrollTo: ScrollTo; + +addOverlays: ( + viewRef: React.ElementRef, + overlayColorsReadOnly: $ReadOnlyArray, + overlayColorsArray: Array, + overlayColorsArrayAnnotation: string[], + ) => void; +} + +export const Commands = codegenNativeCommands({ + supportedCommands: ['scrollTo', 'addOverlays'], +}); + +export default (codegenNativeComponent( + 'Module', +): NativeType); +`; + +const COMMANDS_EVENTS_TYPES_EXPORTED = ` +/** + * 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 + * @flow strict-local + */ + +'use strict'; + +import type { + BubblingEventHandler, + DirectEventHandler, +} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +export type EventInFile = $ReadOnly<{| + ${EVENT_DEFINITION} +|}>; + +export type Boolean = boolean; +export type Int = Int32; +export type Void = void; + +export type ModuleProps = $ReadOnly<{| + ...ViewProps, + + // No props + + // Events defined inline + onBubblingEventDefinedInline: BubblingEventHandler, + onBubblingEventDefinedInlineWithPaperName: BubblingEventHandler, + onDirectEventDefinedInline: DirectEventHandler, + onDirectEventDefinedInlineWithPaperName: DirectEventHandler, +|}>; + +type NativeType = HostComponent; + +export type ScrollTo = (viewRef: React.ElementRef, y: Int, animated: Boolean) => Void; + +interface NativeCommands { + +scrollTo: ScrollTo; +} + +export const Commands = codegenNativeCommands({ + supportedCommands: ['scrollTo'] +}); + +export default (codegenNativeComponent( + 'Module', +): NativeType); +`; + +module.exports = { + ALL_PROP_TYPES_NO_EVENTS, + ARRAY_PROP_TYPES_NO_EVENTS, + OBJECT_PROP_TYPES_NO_EVENTS, + PROPS_ALIASED_LOCALLY, + ONE_OF_EACH_PROP_EVENT_DEFAULT_AND_OPTIONS, + ONE_OF_EACH_PROP_EVENT_DEFAULT_AND_OPTIONS_NO_CAST, + NO_PROPS_EVENTS_ONLY_DEPRECATED_VIEW_CONFIG_NAME_OPTION, + EVENTS_DEFINED_INLINE_WITH_ALL_TYPES, + EVENTS_DEFINED_AS_NULL_INLINE, + PROPS_AND_EVENTS_TYPES_EXPORTED, + COMMANDS_EVENTS_TYPES_EXPORTED, + COMMANDS_DEFINED_WITH_ALL_TYPES, + PROPS_AS_EXTERNAL_TYPES, + COMMANDS_WITH_EXTERNAL_TYPES, +}; diff --git a/packages/react-native-codegen/lib/parsers/flow/components/commands.js b/packages/react-native-codegen/lib/parsers/flow/components/commands.js new file mode 100644 index 00000000000000..2681127cabf515 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/flow/components/commands.js @@ -0,0 +1,131 @@ +/** + * 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 _require = require('../utils.js'), + getValueFromTypes = _require.getValueFromTypes; + +// $FlowFixMe[unclear-type] there's no flowtype for ASTs + +function buildCommandSchema(property, types) { + 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; + case 'Array': + case '$ReadOnlyArray': + if (!paramValue.type === 'GenericTypeAnnotation') { + throw new Error( + 'Array and $ReadOnlyArray are GenericTypeAnnotation for array', + ); + } + returnType = { + type: 'ArrayTypeAnnotation', + elementType: { + // TODO: T172453752 support complex type annotation for array element + type: paramValue.typeParameters.params[0].type, + }, + }; + break; + case 'ArrayTypeAnnotation': + returnType = { + type: 'ArrayTypeAnnotation', + elementType: { + // TODO: T172453752 support complex type annotation for array element + type: paramValue.elementType.type, + }, + }; + break; + default: + type; + throw new Error( + `Unsupported param type for method "${name}", param "${paramName}". Found ${type}`, + ); + } + return { + name: paramName, + optional: false, + typeAnnotation: returnType, + }; + }); + return { + name, + optional, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + params, + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + }, + }; +} +function getCommands(commandTypeAST, types) { + return commandTypeAST + .filter(property => property.type === 'ObjectTypeProperty') + .map(property => buildCommandSchema(property, types)) + .filter(Boolean); +} +module.exports = { + getCommands, +}; diff --git a/packages/react-native-codegen/lib/parsers/flow/components/commands.js.flow b/packages/react-native-codegen/lib/parsers/flow/components/commands.js.flow new file mode 100644 index 00000000000000..c8ce0b157cbc5a --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/flow/components/commands.js.flow @@ -0,0 +1,166 @@ +/** + * 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 { + CommandParamTypeAnnotation, + CommandTypeAnnotation, + NamedShape, +} from '../../../CodegenSchema.js'; +import type {TypeDeclarationMap} from '../../utils'; + +const {getValueFromTypes} = require('../utils.js'); + +// $FlowFixMe[unclear-type] there's no flowtype for ASTs +type EventTypeAST = Object; + +function buildCommandSchema( + property: EventTypeAST, + types: TypeDeclarationMap, +): $ReadOnly<{ + name: string, + optional: boolean, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + params: $ReadOnlyArray<{ + name: string, + optional: boolean, + typeAnnotation: CommandParamTypeAnnotation, + }>, + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + }, +}> { + 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: CommandParamTypeAnnotation; + + 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; + case 'Array': + case '$ReadOnlyArray': + if (!paramValue.type === 'GenericTypeAnnotation') { + throw new Error( + 'Array and $ReadOnlyArray are GenericTypeAnnotation for array', + ); + } + returnType = { + type: 'ArrayTypeAnnotation', + elementType: { + // TODO: T172453752 support complex type annotation for array element + type: paramValue.typeParameters.params[0].type, + }, + }; + break; + case 'ArrayTypeAnnotation': + returnType = { + type: 'ArrayTypeAnnotation', + elementType: { + // TODO: T172453752 support complex type annotation for array element + type: paramValue.elementType.type, + }, + }; + break; + default: + (type: empty); + throw new Error( + `Unsupported param type for method "${name}", param "${paramName}". Found ${type}`, + ); + } + + return { + name: paramName, + optional: false, + 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/lib/parsers/flow/components/componentsUtils.js b/packages/react-native-codegen/lib/parsers/flow/components/componentsUtils.js new file mode 100644 index 00000000000000..b693ba8349091e --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/flow/components/componentsUtils.js @@ -0,0 +1,450 @@ +/** + * 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 _require = require('../../parsers-commons'), + verifyPropNotAlreadyDefined = _require.verifyPropNotAlreadyDefined; +const _require2 = require('../utils.js'), + getValueFromTypes = _require2.getValueFromTypes; + +// $FlowFixMe[unsupported-variance-annotation] +function getTypeAnnotationForArray( + name, + typeAnnotation, + defaultValue, + types, + parser, + buildSchema, +) { + 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' && + parser.getTypeAnnotationName(extractedTypeAnnotation) === '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, + parser, + ) + .map(prop => buildSchema(prop, types, parser)) + .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, + parser, + ) + .map(prop => buildSchema(prop, types, parser)) + .filter(Boolean), + }, + }; + } + } + const type = + extractedTypeAnnotation.type === 'GenericTypeAnnotation' + ? parser.getTypeAnnotationName(extractedTypeAnnotation) + : extractedTypeAnnotation.type; + switch (type) { + 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 '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, + 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: + throw new Error(`Unknown property type for "${name}": ${type}`); + } +} +function flattenProperties(typeDefinition, types, parser) { + return typeDefinition + .map(property => { + if (property.type === 'ObjectTypeProperty') { + return property; + } else if (property.type === 'ObjectTypeSpreadProperty') { + return flattenProperties( + parser.getProperties(property.argument.id.name, types), + types, + parser, + ); + } + }) + .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); +} + +// $FlowFixMe[unsupported-variance-annotation] +function getTypeAnnotation( + name, + annotation, + defaultValue, + withNullDefault, + types, + parser, + buildSchema, +) { + const typeAnnotation = getValueFromTypes(annotation, types); + if ( + typeAnnotation.type === 'GenericTypeAnnotation' && + parser.getTypeAnnotationName(typeAnnotation) === '$ReadOnlyArray' + ) { + return { + type: 'ArrayTypeAnnotation', + elementType: getTypeAnnotationForArray( + name, + typeAnnotation.typeParameters.params[0], + defaultValue, + types, + parser, + buildSchema, + ), + }; + } + if ( + typeAnnotation.type === 'GenericTypeAnnotation' && + parser.getTypeAnnotationName(typeAnnotation) === '$ReadOnly' + ) { + return { + type: 'ObjectTypeAnnotation', + properties: flattenProperties( + typeAnnotation.typeParameters.params[0].properties, + types, + parser, + ) + .map(prop => buildSchema(prop, types, parser)) + .filter(Boolean), + }; + } + const type = + typeAnnotation.type === 'GenericTypeAnnotation' + ? parser.getTypeAnnotationName(typeAnnotation) + : typeAnnotation.type; + switch (type) { + case 'ImageSource': + return { + type: 'ReservedPropTypeAnnotation', + name: 'ImageSourcePrimitive', + }; + case 'ImageRequest': + return { + type: 'ReservedPropTypeAnnotation', + name: 'ImageRequestPrimitive', + }; + 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 'DimensionValue': + return { + type: 'ReservedPropTypeAnnotation', + name: 'DimensionPrimitive', + }; + case 'Int32': + return { + type: 'Int32TypeAnnotation', + default: defaultValue ? defaultValue : 0, + }; + case 'Double': + return { + type: 'DoubleTypeAnnotation', + default: defaultValue ? defaultValue : 0, + }; + case 'Float': + return { + type: 'FloatTypeAnnotation', + default: withNullDefault + ? defaultValue + : defaultValue + ? defaultValue + : 0, + }; + case 'BooleanTypeAnnotation': + return { + type: 'BooleanTypeAnnotation', + default: withNullDefault + ? defaultValue + : defaultValue == null + ? false + : defaultValue, + }; + case 'StringTypeAnnotation': + if (typeof defaultValue !== 'undefined') { + return { + type: 'StringTypeAnnotation', + default: defaultValue, + }; + } + throw new Error(`A default string (or null) is required for "${name}"`); + case 'Stringish': + if (typeof defaultValue !== 'undefined') { + return { + type: 'StringTypeAnnotation', + default: defaultValue, + }; + } + 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, + options: typeAnnotation.types.map(option => option.value), + }; + } else if (unionType === 'NumberLiteralTypeAnnotation') { + return { + type: 'Int32EnumTypeAnnotation', + default: defaultValue, + options: typeAnnotation.types.map(option => option.value), + }; + } else { + throw new Error( + `Unsupported union type for "${name}", received "${unionType}"`, + ); + } + case 'ObjectTypeAnnotation': + throw new Error( + `Cannot use "${type}" type annotation for "${name}": object types must be declared using $ReadOnly<>`, + ); + case 'NumberTypeAnnotation': + throw new Error( + `Cannot use "${type}" type annotation for "${name}": must use a specific numeric type like Int32, Double, or Float`, + ); + case 'UnsafeMixed': + return { + type: 'MixedTypeAnnotation', + }; + default: + throw new Error( + `Unknown property type for "${name}": "${type}" in the State`, + ); + } +} +function getSchemaInfo(property, types) { + 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, + defaultValue, + withNullDefault, + }; +} +module.exports = { + getSchemaInfo, + getTypeAnnotation, + flattenProperties, +}; diff --git a/packages/react-native-codegen/lib/parsers/flow/components/componentsUtils.js.flow b/packages/react-native-codegen/lib/parsers/flow/components/componentsUtils.js.flow new file mode 100644 index 00000000000000..4d6ab405eb8fc0 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/flow/components/componentsUtils.js.flow @@ -0,0 +1,490 @@ +/** + * 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 {BuildSchemaFN, Parser} from '../../parser'; +import type {ASTNode, PropAST, TypeDeclarationMap} from '../../utils'; + +const {verifyPropNotAlreadyDefined} = require('../../parsers-commons'); +const {getValueFromTypes} = require('../utils.js'); + +// $FlowFixMe[unsupported-variance-annotation] +function getTypeAnnotationForArray<+T>( + name: string, + typeAnnotation: $FlowFixMe, + defaultValue: $FlowFixMe | null, + types: TypeDeclarationMap, + parser: Parser, + buildSchema: BuildSchemaFN, +): $FlowFixMe { + 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' && + parser.getTypeAnnotationName(extractedTypeAnnotation) === '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, + parser, + ) + .map(prop => buildSchema(prop, types, parser)) + .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, + parser, + ) + .map(prop => buildSchema(prop, types, parser)) + .filter(Boolean), + }, + }; + } + } + + const type = + extractedTypeAnnotation.type === 'GenericTypeAnnotation' + ? parser.getTypeAnnotationName(extractedTypeAnnotation) + : extractedTypeAnnotation.type; + + switch (type) { + 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 '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: + throw new Error(`Unknown property type for "${name}": ${type}`); + } +} + +function flattenProperties( + typeDefinition: $ReadOnlyArray, + types: TypeDeclarationMap, + parser: Parser, +): $ReadOnlyArray { + return typeDefinition + .map(property => { + if (property.type === 'ObjectTypeProperty') { + return property; + } else if (property.type === 'ObjectTypeSpreadProperty') { + return flattenProperties( + parser.getProperties(property.argument.id.name, types), + types, + parser, + ); + } + }) + .reduce((acc: Array, 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); +} + +// $FlowFixMe[unsupported-variance-annotation] +function getTypeAnnotation<+T>( + name: string, + annotation: $FlowFixMe | ASTNode, + defaultValue: $FlowFixMe | null, + withNullDefault: boolean, + types: TypeDeclarationMap, + parser: Parser, + buildSchema: BuildSchemaFN, +): $FlowFixMe { + const typeAnnotation = getValueFromTypes(annotation, types); + + if ( + typeAnnotation.type === 'GenericTypeAnnotation' && + parser.getTypeAnnotationName(typeAnnotation) === '$ReadOnlyArray' + ) { + return { + type: 'ArrayTypeAnnotation', + elementType: getTypeAnnotationForArray( + name, + typeAnnotation.typeParameters.params[0], + defaultValue, + types, + parser, + buildSchema, + ), + }; + } + + if ( + typeAnnotation.type === 'GenericTypeAnnotation' && + parser.getTypeAnnotationName(typeAnnotation) === '$ReadOnly' + ) { + return { + type: 'ObjectTypeAnnotation', + properties: flattenProperties( + typeAnnotation.typeParameters.params[0].properties, + types, + parser, + ) + .map(prop => buildSchema(prop, types, parser)) + .filter(Boolean), + }; + } + + const type = + typeAnnotation.type === 'GenericTypeAnnotation' + ? parser.getTypeAnnotationName(typeAnnotation) + : typeAnnotation.type; + + switch (type) { + case 'ImageSource': + return { + type: 'ReservedPropTypeAnnotation', + name: 'ImageSourcePrimitive', + }; + case 'ImageRequest': + return { + type: 'ReservedPropTypeAnnotation', + name: 'ImageRequestPrimitive', + }; + 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 'DimensionValue': + return { + type: 'ReservedPropTypeAnnotation', + name: 'DimensionPrimitive', + }; + 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 'ObjectTypeAnnotation': + throw new Error( + `Cannot use "${type}" type annotation for "${name}": object types must be declared using $ReadOnly<>`, + ); + case 'NumberTypeAnnotation': + throw new Error( + `Cannot use "${type}" type annotation for "${name}": must use a specific numeric type like Int32, Double, or Float`, + ); + case 'UnsafeMixed': + return { + type: 'MixedTypeAnnotation', + }; + default: + throw new Error( + `Unknown property type for "${name}": "${type}" in the State`, + ); + } +} + +type SchemaInfo = { + name: string, + optional: boolean, + typeAnnotation: $FlowFixMe, + defaultValue: $FlowFixMe, + withNullDefault: boolean, +}; + +function getSchemaInfo( + property: PropAST, + types: TypeDeclarationMap, +): ?SchemaInfo { + 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, + defaultValue, + withNullDefault, + }; +} + +module.exports = { + getSchemaInfo, + getTypeAnnotation, + flattenProperties, +}; diff --git a/packages/react-native-codegen/lib/parsers/flow/components/events.js b/packages/react-native-codegen/lib/parsers/flow/components/events.js new file mode 100644 index 00000000000000..c0b82916220b89 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/flow/components/events.js @@ -0,0 +1,256 @@ +/** + * 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 _require = require('../../error-utils'), + throwIfArgumentPropsAreNull = _require.throwIfArgumentPropsAreNull, + throwIfBubblingTypeIsNull = _require.throwIfBubblingTypeIsNull, + throwIfEventHasNoName = _require.throwIfEventHasNoName; +const _require2 = require('../../parsers-commons'), + buildPropertiesForEvent = _require2.buildPropertiesForEvent, + emitBuildEventSchema = _require2.emitBuildEventSchema, + getEventArgument = _require2.getEventArgument, + handleEventHandler = _require2.handleEventHandler; +const _require3 = require('../../parsers-primitives'), + emitBoolProp = _require3.emitBoolProp, + emitDoubleProp = _require3.emitDoubleProp, + emitFloatProp = _require3.emitFloatProp, + emitInt32Prop = _require3.emitInt32Prop, + emitMixedProp = _require3.emitMixedProp, + emitObjectProp = _require3.emitObjectProp, + emitStringProp = _require3.emitStringProp, + emitUnionProp = _require3.emitUnionProp; +function getPropertyType( + /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's + * LTI update could not be added via codemod */ + name, + optional, + typeAnnotation, + parser, +) { + const type = extractTypeFromTypeAnnotation(typeAnnotation, parser); + switch (type) { + case 'BooleanTypeAnnotation': + return emitBoolProp(name, optional); + case 'StringTypeAnnotation': + return emitStringProp(name, optional); + case 'Int32': + return emitInt32Prop(name, optional); + case 'Double': + return emitDoubleProp(name, optional); + case 'Float': + return emitFloatProp(name, optional); + case '$ReadOnly': + return getPropertyType( + name, + optional, + typeAnnotation.typeParameters.params[0], + parser, + ); + case 'ObjectTypeAnnotation': + return emitObjectProp( + name, + optional, + parser, + typeAnnotation, + extractArrayElementType, + ); + case 'UnionTypeAnnotation': + return emitUnionProp(name, optional, parser, typeAnnotation); + case 'UnsafeMixed': + return emitMixedProp(name, optional); + case 'ArrayTypeAnnotation': + case '$ReadOnlyArray': + return { + name, + optional, + typeAnnotation: extractArrayElementType(typeAnnotation, name, parser), + }; + default: + throw new Error(`Unable to determine event type for "${name}": ${type}`); + } +} +function extractArrayElementType(typeAnnotation, name, parser) { + const type = extractTypeFromTypeAnnotation(typeAnnotation, parser); + switch (type) { + case 'BooleanTypeAnnotation': + return { + type: 'BooleanTypeAnnotation', + }; + case 'StringTypeAnnotation': + return { + type: 'StringTypeAnnotation', + }; + case 'Int32': + return { + type: 'Int32TypeAnnotation', + }; + case 'Float': + return { + type: 'FloatTypeAnnotation', + }; + case 'NumberTypeAnnotation': + case 'Double': + return { + type: 'DoubleTypeAnnotation', + }; + case 'UnionTypeAnnotation': + return { + type: 'StringEnumTypeAnnotation', + options: typeAnnotation.types.map(option => + parser.getLiteralValue(option), + ), + }; + case 'UnsafeMixed': + return { + type: 'MixedTypeAnnotation', + }; + case 'ObjectTypeAnnotation': + return { + type: 'ObjectTypeAnnotation', + properties: parser + .getObjectProperties(typeAnnotation) + .map(member => + buildPropertiesForEvent(member, parser, getPropertyType), + ), + }; + case 'ArrayTypeAnnotation': + return { + type: 'ArrayTypeAnnotation', + elementType: extractArrayElementType( + typeAnnotation.elementType, + name, + parser, + ), + }; + case '$ReadOnlyArray': + const genericParams = typeAnnotation.typeParameters.params; + if (genericParams.length !== 1) { + throw new Error( + `Events only supports arrays with 1 Generic type. Found ${ + genericParams.length + } types:\n${prettify(genericParams)}`, + ); + } + return { + type: 'ArrayTypeAnnotation', + elementType: extractArrayElementType(genericParams[0], name, parser), + }; + default: + throw new Error( + `Unrecognized ${type} for Array ${name} in events.\n${prettify( + typeAnnotation, + )}`, + ); + } +} +function prettify(jsonObject) { + return JSON.stringify(jsonObject, null, 2); +} +function extractTypeFromTypeAnnotation(typeAnnotation, parser) { + return typeAnnotation.type === 'GenericTypeAnnotation' + ? parser.getTypeAnnotationName(typeAnnotation) + : typeAnnotation.type; +} +function findEventArgumentsAndType( + parser, + typeAnnotation, + types, + bubblingType, + paperName, +) { + throwIfEventHasNoName(typeAnnotation, parser); + const name = parser.getTypeAnnotationName(typeAnnotation); + if (name === '$ReadOnly') { + return { + argumentProps: typeAnnotation.typeParameters.params[0].properties, + paperTopLevelNameDeprecated: paperName, + bubblingType, + }; + } else if (name === 'BubblingEventHandler' || name === 'DirectEventHandler') { + return handleEventHandler( + name, + typeAnnotation, + parser, + types, + findEventArgumentsAndType, + ); + } else if (types[name]) { + return findEventArgumentsAndType( + parser, + types[name].right, + types, + bubblingType, + paperName, + ); + } else { + return { + argumentProps: null, + bubblingType: null, + paperTopLevelNameDeprecated: null, + }; + } +} +function buildEventSchema(types, property, parser) { + 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' || + (parser.getTypeAnnotationName(typeAnnotation) !== 'BubblingEventHandler' && + parser.getTypeAnnotationName(typeAnnotation) !== 'DirectEventHandler') + ) { + return null; + } + const _findEventArgumentsAn = findEventArgumentsAndType( + parser, + typeAnnotation, + types, + ), + argumentProps = _findEventArgumentsAn.argumentProps, + bubblingType = _findEventArgumentsAn.bubblingType, + paperTopLevelNameDeprecated = + _findEventArgumentsAn.paperTopLevelNameDeprecated; + const nonNullableArgumentProps = throwIfArgumentPropsAreNull( + argumentProps, + name, + ); + const nonNullableBubblingType = throwIfBubblingTypeIsNull(bubblingType, name); + const argument = getEventArgument( + nonNullableArgumentProps, + parser, + getPropertyType, + ); + return emitBuildEventSchema( + paperTopLevelNameDeprecated, + name, + optional, + nonNullableBubblingType, + argument, + ); +} + +// $FlowFixMe[unclear-type] there's no flowtype for ASTs + +function getEvents(eventTypeAST, types, parser) { + return eventTypeAST + .filter(property => property.type === 'ObjectTypeProperty') + .map(property => buildEventSchema(types, property, parser)) + .filter(Boolean); +} +module.exports = { + getEvents, + extractArrayElementType, +}; diff --git a/packages/react-native-codegen/lib/parsers/flow/components/events.js.flow b/packages/react-native-codegen/lib/parsers/flow/components/events.js.flow new file mode 100644 index 00000000000000..940706e086c3d2 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/flow/components/events.js.flow @@ -0,0 +1,287 @@ +/** + * 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 { + EventTypeAnnotation, + EventTypeShape, + NamedShape, +} from '../../../CodegenSchema.js'; +import type {Parser} from '../../parser'; +import type {EventArgumentReturnType} from '../../parsers-commons'; + +const { + throwIfArgumentPropsAreNull, + throwIfBubblingTypeIsNull, + throwIfEventHasNoName, +} = require('../../error-utils'); +const { + buildPropertiesForEvent, + emitBuildEventSchema, + getEventArgument, + handleEventHandler, +} = require('../../parsers-commons'); +const { + emitBoolProp, + emitDoubleProp, + emitFloatProp, + emitInt32Prop, + emitMixedProp, + emitObjectProp, + emitStringProp, + emitUnionProp, +} = require('../../parsers-primitives'); + +function getPropertyType( + /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's + * LTI update could not be added via codemod */ + name: string, + optional: boolean, + typeAnnotation: $FlowFixMe, + parser: Parser, +): NamedShape { + const type = extractTypeFromTypeAnnotation(typeAnnotation, parser); + + switch (type) { + case 'BooleanTypeAnnotation': + return emitBoolProp(name, optional); + case 'StringTypeAnnotation': + return emitStringProp(name, optional); + case 'Int32': + return emitInt32Prop(name, optional); + case 'Double': + return emitDoubleProp(name, optional); + case 'Float': + return emitFloatProp(name, optional); + case '$ReadOnly': + return getPropertyType( + name, + optional, + typeAnnotation.typeParameters.params[0], + parser, + ); + case 'ObjectTypeAnnotation': + return emitObjectProp( + name, + optional, + parser, + typeAnnotation, + extractArrayElementType, + ); + case 'UnionTypeAnnotation': + return emitUnionProp(name, optional, parser, typeAnnotation); + case 'UnsafeMixed': + return emitMixedProp(name, optional); + case 'ArrayTypeAnnotation': + case '$ReadOnlyArray': + return { + name, + optional, + typeAnnotation: extractArrayElementType(typeAnnotation, name, parser), + }; + default: + throw new Error(`Unable to determine event type for "${name}": ${type}`); + } +} + +function extractArrayElementType( + typeAnnotation: $FlowFixMe, + name: string, + parser: Parser, +): EventTypeAnnotation { + const type = extractTypeFromTypeAnnotation(typeAnnotation, parser); + + switch (type) { + case 'BooleanTypeAnnotation': + return {type: 'BooleanTypeAnnotation'}; + case 'StringTypeAnnotation': + return {type: 'StringTypeAnnotation'}; + case 'Int32': + return {type: 'Int32TypeAnnotation'}; + case 'Float': + return {type: 'FloatTypeAnnotation'}; + case 'NumberTypeAnnotation': + case 'Double': + return { + type: 'DoubleTypeAnnotation', + }; + case 'UnionTypeAnnotation': + return { + type: 'StringEnumTypeAnnotation', + options: typeAnnotation.types.map(option => + parser.getLiteralValue(option), + ), + }; + case 'UnsafeMixed': + return {type: 'MixedTypeAnnotation'}; + case 'ObjectTypeAnnotation': + return { + type: 'ObjectTypeAnnotation', + properties: parser + .getObjectProperties(typeAnnotation) + .map(member => + buildPropertiesForEvent(member, parser, getPropertyType), + ), + }; + case 'ArrayTypeAnnotation': + return { + type: 'ArrayTypeAnnotation', + elementType: extractArrayElementType( + typeAnnotation.elementType, + name, + parser, + ), + }; + case '$ReadOnlyArray': + const genericParams = typeAnnotation.typeParameters.params; + if (genericParams.length !== 1) { + throw new Error( + `Events only supports arrays with 1 Generic type. Found ${ + genericParams.length + } types:\n${prettify(genericParams)}`, + ); + } + return { + type: 'ArrayTypeAnnotation', + elementType: extractArrayElementType(genericParams[0], name, parser), + }; + default: + throw new Error( + `Unrecognized ${type} for Array ${name} in events.\n${prettify( + typeAnnotation, + )}`, + ); + } +} + +function prettify(jsonObject: $FlowFixMe): string { + return JSON.stringify(jsonObject, null, 2); +} + +function extractTypeFromTypeAnnotation( + typeAnnotation: $FlowFixMe, + parser: Parser, +): string { + return typeAnnotation.type === 'GenericTypeAnnotation' + ? parser.getTypeAnnotationName(typeAnnotation) + : typeAnnotation.type; +} + +function findEventArgumentsAndType( + parser: Parser, + typeAnnotation: $FlowFixMe, + types: TypeMap, + bubblingType: void | 'direct' | 'bubble', + paperName: ?$FlowFixMe, +): EventArgumentReturnType { + throwIfEventHasNoName(typeAnnotation, parser); + const name = parser.getTypeAnnotationName(typeAnnotation); + if (name === '$ReadOnly') { + return { + argumentProps: typeAnnotation.typeParameters.params[0].properties, + paperTopLevelNameDeprecated: paperName, + bubblingType, + }; + } else if (name === 'BubblingEventHandler' || name === 'DirectEventHandler') { + return handleEventHandler( + name, + typeAnnotation, + parser, + types, + findEventArgumentsAndType, + ); + } else if (types[name]) { + return findEventArgumentsAndType( + parser, + types[name].right, + types, + bubblingType, + paperName, + ); + } else { + return { + argumentProps: null, + bubblingType: null, + paperTopLevelNameDeprecated: null, + }; + } +} + +function buildEventSchema( + types: TypeMap, + property: EventTypeAST, + parser: Parser, +): ?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' || + (parser.getTypeAnnotationName(typeAnnotation) !== 'BubblingEventHandler' && + parser.getTypeAnnotationName(typeAnnotation) !== 'DirectEventHandler') + ) { + return null; + } + + const {argumentProps, bubblingType, paperTopLevelNameDeprecated} = + findEventArgumentsAndType(parser, typeAnnotation, types); + + const nonNullableArgumentProps = throwIfArgumentPropsAreNull( + argumentProps, + name, + ); + const nonNullableBubblingType = throwIfBubblingTypeIsNull(bubblingType, name); + + const argument = getEventArgument( + nonNullableArgumentProps, + parser, + getPropertyType, + ); + + return emitBuildEventSchema( + paperTopLevelNameDeprecated, + name, + optional, + nonNullableBubblingType, + argument, + ); +} + +// $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, + parser: Parser, +): $ReadOnlyArray { + return eventTypeAST + .filter(property => property.type === 'ObjectTypeProperty') + .map(property => buildEventSchema(types, property, parser)) + .filter(Boolean); +} + +module.exports = { + getEvents, + extractArrayElementType, +}; diff --git a/packages/react-native-codegen/lib/parsers/flow/components/index.js b/packages/react-native-codegen/lib/parsers/flow/components/index.js new file mode 100644 index 00000000000000..de65761a80925c --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/flow/components/index.js @@ -0,0 +1,49 @@ +/** + * 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 _require = require('../../parsers-commons'), + findComponentConfig = _require.findComponentConfig, + getCommandProperties = _require.getCommandProperties, + getOptions = _require.getOptions; +const _require2 = require('./commands'), + getCommands = _require2.getCommands; +const _require3 = require('./events'), + getEvents = _require3.getEvents; + +// $FlowFixMe[signature-verification-failure] there's no flowtype for AST +function buildComponentSchema(ast, parser) { + const _findComponentConfig = findComponentConfig(ast, parser), + componentName = _findComponentConfig.componentName, + propsTypeName = _findComponentConfig.propsTypeName, + optionsExpression = _findComponentConfig.optionsExpression; + const types = parser.getTypes(ast); + const propProperties = parser.getProperties(propsTypeName, types); + const commandProperties = getCommandProperties(ast, parser); + const _parser$getProps = parser.getProps(propProperties, types), + extendsProps = _parser$getProps.extendsProps, + props = _parser$getProps.props; + const options = getOptions(optionsExpression); + const events = getEvents(propProperties, types, parser); + const commands = getCommands(commandProperties, types); + return { + filename: componentName, + componentName, + options, + extendsProps, + events, + props, + commands, + }; +} +module.exports = { + buildComponentSchema, +}; diff --git a/packages/react-native-codegen/lib/parsers/flow/components/index.js.flow b/packages/react-native-codegen/lib/parsers/flow/components/index.js.flow new file mode 100644 index 00000000000000..0cae27b0607946 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/flow/components/index.js.flow @@ -0,0 +1,56 @@ +/** + * 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 {Parser} from '../../parser'; +import type {ComponentSchemaBuilderConfig} from '../../schema.js'; + +const { + findComponentConfig, + getCommandProperties, + getOptions, +} = require('../../parsers-commons'); +const {getCommands} = require('./commands'); +const {getEvents} = require('./events'); + +// $FlowFixMe[signature-verification-failure] there's no flowtype for AST +function buildComponentSchema( + ast: $FlowFixMe, + parser: Parser, +): ComponentSchemaBuilderConfig { + const {componentName, propsTypeName, optionsExpression} = findComponentConfig( + ast, + parser, + ); + + const types = parser.getTypes(ast); + + const propProperties = parser.getProperties(propsTypeName, types); + const commandProperties = getCommandProperties(ast, parser); + const {extendsProps, props} = parser.getProps(propProperties, types); + + const options = getOptions(optionsExpression); + const events = getEvents(propProperties, types, parser); + const commands = getCommands(commandProperties, types); + + return { + filename: componentName, + componentName, + options, + extendsProps, + events, + props, + commands, + }; +} + +module.exports = { + buildComponentSchema, +}; diff --git a/packages/react-native-codegen/lib/parsers/flow/modules/__test_fixtures__/failures.js b/packages/react-native-codegen/lib/parsers/flow/modules/__test_fixtures__/failures.js new file mode 100644 index 00000000000000..59309dcc7a1e14 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/flow/modules/__test_fixtures__/failures.js @@ -0,0 +1,269 @@ +/** + * 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 + */ + +// @licenselint-loose-mode + +'use strict'; + +const NATIVE_MODULES_WITH_ARRAY_WITH_NO_TYPE_FOR_CONTENT = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + getString: (arg: string) => Array; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; +const NATIVE_MODULES_WITH_ARRAY_WITH_NO_TYPE_FOR_CONTENT_AS_PARAM = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + getString: (arg : Array) => string; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; +const NATIVE_MODULES_WITH_READ_ONLY_OBJECT_NO_TYPE_FOR_CONTENT = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + getString: (arg : $ReadOnly<>) => string; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; +const NATIVE_MODULES_WITH_NOT_ONLY_METHODS = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +getBool: (arg: boolean) => boolean; + +getNumber: (arg: number) => number; + +getString: (arg: string) => string; + sampleBool: boolean, + +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; +const NATIVE_MODULES_WITH_UNNAMED_PARAMS = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +getBool: (boolean) => boolean; + +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; +const NATIVE_MODULES_WITH_PROMISE_WITHOUT_TYPE = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +getBool: (arg: boolean) => Promise; + +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; +const TWO_NATIVE_MODULES_EXPORTED_WITH_DEFAULT = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule1'); +export default TurboModuleRegistry.getEnforcing('SampleTurboModule2'); + +`; +const TWO_NATIVE_EXTENDING_TURBO_MODULE = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +getSth: (a : ?number) => void +} + +export interface Spec2 extends TurboModule { + +getSth: (a : ?number) => void +} + + +`; +const EMPTY_ENUM_NATIVE_MODULE = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export enum SomeEnum { +} + +export interface Spec extends TurboModule { + +getEnums: (a: SomeEnum) => string; +} + +export default TurboModuleRegistry.getEnforcing('EmptyEnumNativeModule'); +`; +const MIXED_VALUES_ENUM_NATIVE_MODULE = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export enum SomeEnum { + NUM = 1, + STR = 'str', +} + +export interface Spec extends TurboModule { + +getEnums: (a: SomeEnum) => string; +} + +export default TurboModuleRegistry.getEnforcing('MixedValuesEnumNativeModule'); +`; +module.exports = { + NATIVE_MODULES_WITH_READ_ONLY_OBJECT_NO_TYPE_FOR_CONTENT, + NATIVE_MODULES_WITH_UNNAMED_PARAMS, + NATIVE_MODULES_WITH_PROMISE_WITHOUT_TYPE, + NATIVE_MODULES_WITH_ARRAY_WITH_NO_TYPE_FOR_CONTENT_AS_PARAM, + NATIVE_MODULES_WITH_ARRAY_WITH_NO_TYPE_FOR_CONTENT, + TWO_NATIVE_MODULES_EXPORTED_WITH_DEFAULT, + NATIVE_MODULES_WITH_NOT_ONLY_METHODS, + TWO_NATIVE_EXTENDING_TURBO_MODULE, + EMPTY_ENUM_NATIVE_MODULE, + MIXED_VALUES_ENUM_NATIVE_MODULE, +}; diff --git a/packages/react-native-codegen/lib/parsers/flow/modules/__test_fixtures__/failures.js.flow b/packages/react-native-codegen/lib/parsers/flow/modules/__test_fixtures__/failures.js.flow new file mode 100644 index 00000000000000..8734095f43f2d4 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/flow/modules/__test_fixtures__/failures.js.flow @@ -0,0 +1,279 @@ +/** + * 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-local + * @format + */ + +// @licenselint-loose-mode + +'use strict'; + +const NATIVE_MODULES_WITH_ARRAY_WITH_NO_TYPE_FOR_CONTENT = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + getString: (arg: string) => Array; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULES_WITH_ARRAY_WITH_NO_TYPE_FOR_CONTENT_AS_PARAM = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + getString: (arg : Array) => string; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULES_WITH_READ_ONLY_OBJECT_NO_TYPE_FOR_CONTENT = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + getString: (arg : $ReadOnly<>) => string; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULES_WITH_NOT_ONLY_METHODS = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +getBool: (arg: boolean) => boolean; + +getNumber: (arg: number) => number; + +getString: (arg: string) => string; + sampleBool: boolean, + +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULES_WITH_UNNAMED_PARAMS = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +getBool: (boolean) => boolean; + +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULES_WITH_PROMISE_WITHOUT_TYPE = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +getBool: (arg: boolean) => Promise; + +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const TWO_NATIVE_MODULES_EXPORTED_WITH_DEFAULT = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule1'); +export default TurboModuleRegistry.getEnforcing('SampleTurboModule2'); + +`; + +const TWO_NATIVE_EXTENDING_TURBO_MODULE = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +getSth: (a : ?number) => void +} + +export interface Spec2 extends TurboModule { + +getSth: (a : ?number) => void +} + + +`; + +const EMPTY_ENUM_NATIVE_MODULE = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export enum SomeEnum { +} + +export interface Spec extends TurboModule { + +getEnums: (a: SomeEnum) => string; +} + +export default TurboModuleRegistry.getEnforcing('EmptyEnumNativeModule'); +`; + +const MIXED_VALUES_ENUM_NATIVE_MODULE = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export enum SomeEnum { + NUM = 1, + STR = 'str', +} + +export interface Spec extends TurboModule { + +getEnums: (a: SomeEnum) => string; +} + +export default TurboModuleRegistry.getEnforcing('MixedValuesEnumNativeModule'); +`; + +module.exports = { + NATIVE_MODULES_WITH_READ_ONLY_OBJECT_NO_TYPE_FOR_CONTENT, + NATIVE_MODULES_WITH_UNNAMED_PARAMS, + NATIVE_MODULES_WITH_PROMISE_WITHOUT_TYPE, + NATIVE_MODULES_WITH_ARRAY_WITH_NO_TYPE_FOR_CONTENT_AS_PARAM, + NATIVE_MODULES_WITH_ARRAY_WITH_NO_TYPE_FOR_CONTENT, + TWO_NATIVE_MODULES_EXPORTED_WITH_DEFAULT, + NATIVE_MODULES_WITH_NOT_ONLY_METHODS, + TWO_NATIVE_EXTENDING_TURBO_MODULE, + EMPTY_ENUM_NATIVE_MODULE, + MIXED_VALUES_ENUM_NATIVE_MODULE, +}; diff --git a/packages/react-native-codegen/lib/parsers/flow/modules/__test_fixtures__/fixtures.js b/packages/react-native-codegen/lib/parsers/flow/modules/__test_fixtures__/fixtures.js new file mode 100644 index 00000000000000..4b17a2bfbf230e --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/flow/modules/__test_fixtures__/fixtures.js @@ -0,0 +1,796 @@ +/** + * 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 + */ + +// @licenselint-loose-mode + +'use strict'; + +const EMPTY_NATIVE_MODULE = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + // no methods +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; +const NATIVE_MODULE_WITH_COMPLEX_OBJECTS = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export type String = string; + +export interface Spec extends TurboModule { + // Exported methods. + +getObject: (arg: {|const1: {|const1: boolean|}|}) => {| + const1: {|const1: boolean|}, + |}; + +getReadOnlyObject: (arg: $ReadOnly<{|const1: $ReadOnly<{|const1: boolean|}>|}>) => $ReadOnly<{| + const1: {|const1: boolean|}, + |}>; + +getObject2: (arg: { a: String }) => Object; + +getObjectInArray: (arg: {const1: {|const1: boolean|}}) => Array<{| + const1: {const1: boolean}, + |}>; +} +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; +const NATIVE_MODULE_WITH_COMPLEX_OBJECTS_WITH_NULLABLE_KEY = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +getConstants: () => {| + isTesting: boolean, + reactNativeVersion: {| + major: number, + minor: number, + patch?: number, + prerelease: ?number, + |}, + forceTouchAvailable: boolean, + osVersion: string, + systemName: string, + interfaceIdiom: string, + |}; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; +const NATIVE_MODULE_WITH_BASIC_PARAM_TYPES = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +passBool?: (arg: boolean) => void; + +passNumber: (arg: number) => void; + +passString: (arg: string) => void; + +passStringish: (arg: Stringish) => void; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; +const NATIVE_MODULE_WITH_ALIASES = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +type NumNum = number; +export type Num = (arg: NumNum) => void; +type Num2 = Num; +export type Void = void; +export type A = number; +export type B = number; +export type ObjectAlias = {| + x: number, + y: number, + label: string, + truthy: boolean, +|}; +export type ReadOnlyAlias = $ReadOnly; + +export interface Spec extends TurboModule { + // Exported methods. + +getNumber: Num2; + +getVoid: () => Void; + +getArray: (a: Array) => {| a: B |}; + +getStringFromAlias: (a: ObjectAlias) => string; + +getStringFromNullableAlias: (a: ?ObjectAlias) => string; + +getStringFromReadOnlyAlias: (a: ReadOnlyAlias) => string; + +getStringFromNullableReadOnlyAlias: (a: ?ReadOnlyAlias) => string; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; +const NATIVE_MODULE_WITH_NESTED_ALIASES = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +type Bar = {| + z: number +|}; + +type Foo = {| + bar1: Bar, + bar2: Bar, +|}; + +export interface Spec extends TurboModule { + // Exported methods. + foo1: (x: Foo) => Foo; + foo2: (x: Foo) => void; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; +const NATIVE_MODULE_WITH_FLOAT_AND_INT32 = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; +import type {Int32, Float} from 'react-native/Libraries/Types/CodegenTypes'; + +export interface Spec extends TurboModule { + +getInt: (arg: Int32) => Int32; + +getFloat: (arg: Float) => Float; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; +const NATIVE_MODULE_WITH_SIMPLE_OBJECT = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +getObject: (o: Object) => Object, +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; +const NATIVE_MODULE_WITH_UNSAFE_OBJECT = ` +/** + * 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 {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; +import type {UnsafeObject} from 'react-native/Libraries/Types/CodegenTypes'; + +export interface Spec extends TurboModule { + +getUnsafeObject: (o: UnsafeObject) => UnsafeObject, +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; +const NATIVE_MODULE_WITH_PARTIALS = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export type SomeObj = {| + a: string, + b?: boolean, +|}; + +export interface Spec extends TurboModule { + +getSomeObj: () => SomeObj; + +getPartialSomeObj: () => Partial; + +getSomeObjFromPartialSomeObj: (value: Partial) => SomeObj; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; +const NATIVE_MODULE_WITH_PARTIALS_COMPLEX = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export type SomeObj = {| + a: string, + b?: boolean, +|}; + +export type PartialSomeObj = Partial; + +export interface Spec extends TurboModule { + +getPartialPartial: (value1: Partial, value2: PartialSomeObj) => SomeObj +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; +const NATIVE_MODULE_WITH_ROOT_TAG = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {RootTag, TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +getRootTag: (rootTag: RootTag) => RootTag, +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; +const NATIVE_MODULE_WITH_NULLABLE_PARAM = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + // Exported methods. + +voidFunc: (arg: ?string) => void; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; +const NATIVE_MODULE_WITH_BASIC_ARRAY = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +getArray: (arg: Array) => Array; + +getArray: (arg: $ReadOnlyArray) => $ReadOnlyArray; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; +const NATIVE_MODULE_WITH_OBJECT_WITH_OBJECT_DEFINED_IN_FILE_AS_PROPERTY = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +type DisplayMetricsAndroid = {| + width: number, +|}; + +export interface Spec extends TurboModule { + +getConstants: () => {| + +Dimensions: { + windowPhysicalPixels: DisplayMetricsAndroid, + }, + |}; + +getConstants2: () => $ReadOnly<{| + +Dimensions: { + windowPhysicalPixels: DisplayMetricsAndroid, + }, + |}>; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; +const NATIVE_MODULE_WITH_ARRAY_WITH_UNION_AND_TOUPLE = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +getArray: (arg: Array<[string, string]>) => Array; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; +const NATIVE_MODULE_WITH_ARRAY_WITH_ALIAS = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export type SomeString = string; + +export interface Spec extends TurboModule { + +getArray: (arg: Array) => Array; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; +const NATIVE_MODULE_WITH_COMPLEX_ARRAY = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +getArray: (arg: Array>>>>) => Array>>; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; +const NATIVE_MODULE_WITH_PROMISE = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export type String = string; +export type SomeObj = {| a: string |}; + +export interface Spec extends TurboModule { + +getValueWithPromise: () => Promise; + +getValueWithPromiseDefinedSomewhereElse: () => Promise; + +getValueWithPromiseObjDefinedSomewhereElse: () => Promise; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; +const NATIVE_MODULE_WITH_CALLBACK = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + // Exported methods. + +getValueWithCallback: ( + callback: (value: string, arr: Array>) => void, + ) => void; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; +const NATIVE_MODULE_WITH_UNION = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export type ChooseInt = 1 | 2 | 3; +export type ChooseFloat = 1.44 | 2.88 | 5.76; +export type ChooseObject = {} | {low: string}; +export type ChooseString = 'One' | 'Two' | 'Three'; + +export interface Spec extends TurboModule { + +getUnion: (chooseInt: ChooseInt, chooseFloat: ChooseFloat, chooseObject: ChooseObject, chooseString: ChooseString) => ChooseObject; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; +const ANDROID_ONLY_NATIVE_MODULE = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + // no methods +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModuleAndroid'); + +`; +const IOS_ONLY_NATIVE_MODULE = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export enum Quality { + SD, + HD, +} + +export enum Resolution { + Low = 720, + High = 1080, +} + +export enum StringOptions { + One = 'one', + Two = 'two', + Three = 'three', +} + +export interface Spec extends TurboModule { + getEnums(quality: Quality, resolution?: Resolution, stringOptions: StringOptions): string; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModuleIOS'); + +`; +const CXX_ONLY_NATIVE_MODULE = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export type ChooseInt = 1 | 2 | 3; +export type ChooseFloat = 1.44 | 2.88 | 5.76; +export type ChooseObject = {} | {low: string}; +export type ChooseString = 'One' | 'Two' | 'Three'; + +export enum Quality { + SD, + HD, +} + +export enum Resolution { + Low = 720, + High = 1080, +} + +export enum StringOptions { + One = 'one', + Two = 'two', + Three = 'three', +} + +export type BinaryTreeNode = { + left?: BinaryTreeNode, + value: number, + right?: BinaryTreeNode, +}; + +export type GraphNode = { + label: string, + neighbors?: Array, +}; + +export type CustomDeviceEvent = { + type: string, + level: number, + degree?: number, +}; + +export interface Spec extends TurboModule { + +getCallback: () => () => void; + +getMixed: (arg: mixed) => mixed; + +getEnums: (quality: Quality, resolution?: Resolution, stringOptions: StringOptions) => string; + +getBinaryTreeNode: (arg: BinaryTreeNode) => BinaryTreeNode; + +getGraphNode: (arg: GraphNode) => GraphNode; + +getMap: (arg: {[a: string]: ?number}) => {[b: string]: ?number}; + +getAnotherMap: (arg: {[string]: string}) => {[string]: string}; + +getUnion: (chooseInt: ChooseInt, chooseFloat: ChooseFloat, chooseObject: ChooseObject, chooseString: ChooseString) => ChooseObject; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModuleCxx'); + +`; +const PROMISE_WITH_COMMONLY_USED_TYPES = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export type Season = 'Spring' | 'Summer' | 'Autumn' | 'Winter'; + +export type CustomObject = {| + field1: Array, + field2: boolean, + field3: string, + type: 'A_String_Literal', +|}; + +export interface Spec extends TurboModule { + returnStringArray(): Promise>; + returnObjectArray(): Promise>; + returnNullableNumber(): Promise; + returnEmpty(): Promise; + returnUnsupportedIndex(): Promise<{ [string]: 'authorized' | 'denied' | 'undetermined' | true | false }>; + returnSupportedIndex(): Promise<{ [string]: CustomObject }>; + returnEnum() : Promise; + returnObject() : Promise; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; +module.exports = { + NATIVE_MODULE_WITH_OBJECT_WITH_OBJECT_DEFINED_IN_FILE_AS_PROPERTY, + NATIVE_MODULE_WITH_ARRAY_WITH_UNION_AND_TOUPLE, + NATIVE_MODULE_WITH_FLOAT_AND_INT32, + NATIVE_MODULE_WITH_ALIASES, + NATIVE_MODULE_WITH_NESTED_ALIASES, + NATIVE_MODULE_WITH_PROMISE, + NATIVE_MODULE_WITH_COMPLEX_OBJECTS, + NATIVE_MODULE_WITH_COMPLEX_OBJECTS_WITH_NULLABLE_KEY, + NATIVE_MODULE_WITH_SIMPLE_OBJECT, + NATIVE_MODULE_WITH_UNSAFE_OBJECT, + NATIVE_MODULE_WITH_PARTIALS, + NATIVE_MODULE_WITH_PARTIALS_COMPLEX, + NATIVE_MODULE_WITH_ROOT_TAG, + NATIVE_MODULE_WITH_NULLABLE_PARAM, + NATIVE_MODULE_WITH_BASIC_ARRAY, + NATIVE_MODULE_WITH_COMPLEX_ARRAY, + NATIVE_MODULE_WITH_ARRAY_WITH_ALIAS, + NATIVE_MODULE_WITH_BASIC_PARAM_TYPES, + NATIVE_MODULE_WITH_CALLBACK, + NATIVE_MODULE_WITH_UNION, + EMPTY_NATIVE_MODULE, + ANDROID_ONLY_NATIVE_MODULE, + IOS_ONLY_NATIVE_MODULE, + CXX_ONLY_NATIVE_MODULE, + PROMISE_WITH_COMMONLY_USED_TYPES, +}; diff --git a/packages/react-native-codegen/lib/parsers/flow/modules/__test_fixtures__/fixtures.js.flow b/packages/react-native-codegen/lib/parsers/flow/modules/__test_fixtures__/fixtures.js.flow new file mode 100644 index 00000000000000..534a2ceedb7066 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/flow/modules/__test_fixtures__/fixtures.js.flow @@ -0,0 +1,821 @@ +/** + * 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-local + * @format + */ + +// @licenselint-loose-mode + +'use strict'; + +const EMPTY_NATIVE_MODULE = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + // no methods +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULE_WITH_COMPLEX_OBJECTS = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export type String = string; + +export interface Spec extends TurboModule { + // Exported methods. + +getObject: (arg: {|const1: {|const1: boolean|}|}) => {| + const1: {|const1: boolean|}, + |}; + +getReadOnlyObject: (arg: $ReadOnly<{|const1: $ReadOnly<{|const1: boolean|}>|}>) => $ReadOnly<{| + const1: {|const1: boolean|}, + |}>; + +getObject2: (arg: { a: String }) => Object; + +getObjectInArray: (arg: {const1: {|const1: boolean|}}) => Array<{| + const1: {const1: boolean}, + |}>; +} +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULE_WITH_COMPLEX_OBJECTS_WITH_NULLABLE_KEY = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +getConstants: () => {| + isTesting: boolean, + reactNativeVersion: {| + major: number, + minor: number, + patch?: number, + prerelease: ?number, + |}, + forceTouchAvailable: boolean, + osVersion: string, + systemName: string, + interfaceIdiom: string, + |}; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULE_WITH_BASIC_PARAM_TYPES = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +passBool?: (arg: boolean) => void; + +passNumber: (arg: number) => void; + +passString: (arg: string) => void; + +passStringish: (arg: Stringish) => void; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULE_WITH_ALIASES = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +type NumNum = number; +export type Num = (arg: NumNum) => void; +type Num2 = Num; +export type Void = void; +export type A = number; +export type B = number; +export type ObjectAlias = {| + x: number, + y: number, + label: string, + truthy: boolean, +|}; +export type ReadOnlyAlias = $ReadOnly; + +export interface Spec extends TurboModule { + // Exported methods. + +getNumber: Num2; + +getVoid: () => Void; + +getArray: (a: Array) => {| a: B |}; + +getStringFromAlias: (a: ObjectAlias) => string; + +getStringFromNullableAlias: (a: ?ObjectAlias) => string; + +getStringFromReadOnlyAlias: (a: ReadOnlyAlias) => string; + +getStringFromNullableReadOnlyAlias: (a: ?ReadOnlyAlias) => string; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULE_WITH_NESTED_ALIASES = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +type Bar = {| + z: number +|}; + +type Foo = {| + bar1: Bar, + bar2: Bar, +|}; + +export interface Spec extends TurboModule { + // Exported methods. + foo1: (x: Foo) => Foo; + foo2: (x: Foo) => void; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULE_WITH_FLOAT_AND_INT32 = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; +import type {Int32, Float} from 'react-native/Libraries/Types/CodegenTypes'; + +export interface Spec extends TurboModule { + +getInt: (arg: Int32) => Int32; + +getFloat: (arg: Float) => Float; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; + +const NATIVE_MODULE_WITH_SIMPLE_OBJECT = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +getObject: (o: Object) => Object, +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULE_WITH_UNSAFE_OBJECT = ` +/** + * 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 {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; +import type {UnsafeObject} from 'react-native/Libraries/Types/CodegenTypes'; + +export interface Spec extends TurboModule { + +getUnsafeObject: (o: UnsafeObject) => UnsafeObject, +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULE_WITH_PARTIALS = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export type SomeObj = {| + a: string, + b?: boolean, +|}; + +export interface Spec extends TurboModule { + +getSomeObj: () => SomeObj; + +getPartialSomeObj: () => Partial; + +getSomeObjFromPartialSomeObj: (value: Partial) => SomeObj; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULE_WITH_PARTIALS_COMPLEX = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export type SomeObj = {| + a: string, + b?: boolean, +|}; + +export type PartialSomeObj = Partial; + +export interface Spec extends TurboModule { + +getPartialPartial: (value1: Partial, value2: PartialSomeObj) => SomeObj +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULE_WITH_ROOT_TAG = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {RootTag, TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +getRootTag: (rootTag: RootTag) => RootTag, +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULE_WITH_NULLABLE_PARAM = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + // Exported methods. + +voidFunc: (arg: ?string) => void; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULE_WITH_BASIC_ARRAY = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +getArray: (arg: Array) => Array; + +getArray: (arg: $ReadOnlyArray) => $ReadOnlyArray; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULE_WITH_OBJECT_WITH_OBJECT_DEFINED_IN_FILE_AS_PROPERTY = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +type DisplayMetricsAndroid = {| + width: number, +|}; + +export interface Spec extends TurboModule { + +getConstants: () => {| + +Dimensions: { + windowPhysicalPixels: DisplayMetricsAndroid, + }, + |}; + +getConstants2: () => $ReadOnly<{| + +Dimensions: { + windowPhysicalPixels: DisplayMetricsAndroid, + }, + |}>; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULE_WITH_ARRAY_WITH_UNION_AND_TOUPLE = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +getArray: (arg: Array<[string, string]>) => Array; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULE_WITH_ARRAY_WITH_ALIAS = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export type SomeString = string; + +export interface Spec extends TurboModule { + +getArray: (arg: Array) => Array; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULE_WITH_COMPLEX_ARRAY = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +getArray: (arg: Array>>>>) => Array>>; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULE_WITH_PROMISE = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export type String = string; +export type SomeObj = {| a: string |}; + +export interface Spec extends TurboModule { + +getValueWithPromise: () => Promise; + +getValueWithPromiseDefinedSomewhereElse: () => Promise; + +getValueWithPromiseObjDefinedSomewhereElse: () => Promise; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULE_WITH_CALLBACK = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + // Exported methods. + +getValueWithCallback: ( + callback: (value: string, arr: Array>) => void, + ) => void; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULE_WITH_UNION = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export type ChooseInt = 1 | 2 | 3; +export type ChooseFloat = 1.44 | 2.88 | 5.76; +export type ChooseObject = {} | {low: string}; +export type ChooseString = 'One' | 'Two' | 'Three'; + +export interface Spec extends TurboModule { + +getUnion: (chooseInt: ChooseInt, chooseFloat: ChooseFloat, chooseObject: ChooseObject, chooseString: ChooseString) => ChooseObject; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const ANDROID_ONLY_NATIVE_MODULE = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + // no methods +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModuleAndroid'); + +`; + +const IOS_ONLY_NATIVE_MODULE = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export enum Quality { + SD, + HD, +} + +export enum Resolution { + Low = 720, + High = 1080, +} + +export enum StringOptions { + One = 'one', + Two = 'two', + Three = 'three', +} + +export interface Spec extends TurboModule { + getEnums(quality: Quality, resolution?: Resolution, stringOptions: StringOptions): string; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModuleIOS'); + +`; + +const CXX_ONLY_NATIVE_MODULE = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export type ChooseInt = 1 | 2 | 3; +export type ChooseFloat = 1.44 | 2.88 | 5.76; +export type ChooseObject = {} | {low: string}; +export type ChooseString = 'One' | 'Two' | 'Three'; + +export enum Quality { + SD, + HD, +} + +export enum Resolution { + Low = 720, + High = 1080, +} + +export enum StringOptions { + One = 'one', + Two = 'two', + Three = 'three', +} + +export type BinaryTreeNode = { + left?: BinaryTreeNode, + value: number, + right?: BinaryTreeNode, +}; + +export type GraphNode = { + label: string, + neighbors?: Array, +}; + +export type CustomDeviceEvent = { + type: string, + level: number, + degree?: number, +}; + +export interface Spec extends TurboModule { + +getCallback: () => () => void; + +getMixed: (arg: mixed) => mixed; + +getEnums: (quality: Quality, resolution?: Resolution, stringOptions: StringOptions) => string; + +getBinaryTreeNode: (arg: BinaryTreeNode) => BinaryTreeNode; + +getGraphNode: (arg: GraphNode) => GraphNode; + +getMap: (arg: {[a: string]: ?number}) => {[b: string]: ?number}; + +getAnotherMap: (arg: {[string]: string}) => {[string]: string}; + +getUnion: (chooseInt: ChooseInt, chooseFloat: ChooseFloat, chooseObject: ChooseObject, chooseString: ChooseString) => ChooseObject; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModuleCxx'); + +`; + +const PROMISE_WITH_COMMONLY_USED_TYPES = ` +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export type Season = 'Spring' | 'Summer' | 'Autumn' | 'Winter'; + +export type CustomObject = {| + field1: Array, + field2: boolean, + field3: string, + type: 'A_String_Literal', +|}; + +export interface Spec extends TurboModule { + returnStringArray(): Promise>; + returnObjectArray(): Promise>; + returnNullableNumber(): Promise; + returnEmpty(): Promise; + returnUnsupportedIndex(): Promise<{ [string]: 'authorized' | 'denied' | 'undetermined' | true | false }>; + returnSupportedIndex(): Promise<{ [string]: CustomObject }>; + returnEnum() : Promise; + returnObject() : Promise; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; + +module.exports = { + NATIVE_MODULE_WITH_OBJECT_WITH_OBJECT_DEFINED_IN_FILE_AS_PROPERTY, + NATIVE_MODULE_WITH_ARRAY_WITH_UNION_AND_TOUPLE, + NATIVE_MODULE_WITH_FLOAT_AND_INT32, + NATIVE_MODULE_WITH_ALIASES, + NATIVE_MODULE_WITH_NESTED_ALIASES, + NATIVE_MODULE_WITH_PROMISE, + NATIVE_MODULE_WITH_COMPLEX_OBJECTS, + NATIVE_MODULE_WITH_COMPLEX_OBJECTS_WITH_NULLABLE_KEY, + NATIVE_MODULE_WITH_SIMPLE_OBJECT, + NATIVE_MODULE_WITH_UNSAFE_OBJECT, + NATIVE_MODULE_WITH_PARTIALS, + NATIVE_MODULE_WITH_PARTIALS_COMPLEX, + NATIVE_MODULE_WITH_ROOT_TAG, + NATIVE_MODULE_WITH_NULLABLE_PARAM, + NATIVE_MODULE_WITH_BASIC_ARRAY, + NATIVE_MODULE_WITH_COMPLEX_ARRAY, + NATIVE_MODULE_WITH_ARRAY_WITH_ALIAS, + NATIVE_MODULE_WITH_BASIC_PARAM_TYPES, + NATIVE_MODULE_WITH_CALLBACK, + NATIVE_MODULE_WITH_UNION, + EMPTY_NATIVE_MODULE, + ANDROID_ONLY_NATIVE_MODULE, + IOS_ONLY_NATIVE_MODULE, + CXX_ONLY_NATIVE_MODULE, + PROMISE_WITH_COMMONLY_USED_TYPES, +}; diff --git a/packages/react-native-codegen/lib/parsers/flow/modules/index.js b/packages/react-native-codegen/lib/parsers/flow/modules/index.js new file mode 100644 index 00000000000000..605e0c0e923e8e --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/flow/modules/index.js @@ -0,0 +1,343 @@ +/** + * 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'; + +function _slicedToArray(r, e) { + return ( + _arrayWithHoles(r) || + _iterableToArrayLimit(r, e) || + _unsupportedIterableToArray(r, e) || + _nonIterableRest() + ); +} +function _nonIterableRest() { + throw new TypeError( + 'Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.', + ); +} +function _unsupportedIterableToArray(r, a) { + if (r) { + if ('string' == typeof r) return _arrayLikeToArray(r, a); + var t = {}.toString.call(r).slice(8, -1); + return ( + 'Object' === t && r.constructor && (t = r.constructor.name), + 'Map' === t || 'Set' === t + ? Array.from(r) + : 'Arguments' === t || + /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) + ? _arrayLikeToArray(r, a) + : void 0 + ); + } +} +function _arrayLikeToArray(r, a) { + (null == a || a > r.length) && (a = r.length); + for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; + return n; +} +function _iterableToArrayLimit(r, l) { + var t = + null == r + ? null + : ('undefined' != typeof Symbol && r[Symbol.iterator]) || r['@@iterator']; + if (null != t) { + var e, + n, + i, + u, + a = [], + f = !0, + o = !1; + try { + if (((i = (t = t.call(r)).next), 0 === l)) { + if (Object(t) !== t) return; + f = !1; + } else + for ( + ; + !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); + f = !0 + ); + } catch (r) { + (o = !0), (n = r); + } finally { + try { + if (!f && null != t.return && ((u = t.return()), Object(u) !== u)) + return; + } finally { + if (o) throw n; + } + } + return a; + } +} +function _arrayWithHoles(r) { + if (Array.isArray(r)) return r; +} +const _require = require('../../errors'), + UnsupportedEnumDeclarationParserError = + _require.UnsupportedEnumDeclarationParserError, + UnsupportedGenericParserError = _require.UnsupportedGenericParserError, + UnsupportedTypeAnnotationParserError = + _require.UnsupportedTypeAnnotationParserError; +const _require2 = require('../../parsers-commons'), + assertGenericTypeAnnotationHasExactlyOneTypeParameter = + _require2.assertGenericTypeAnnotationHasExactlyOneTypeParameter, + parseObjectProperty = _require2.parseObjectProperty, + unwrapNullable = _require2.unwrapNullable, + wrapNullable = _require2.wrapNullable; +const _require3 = require('../../parsers-primitives'), + emitArrayType = _require3.emitArrayType, + emitCommonTypes = _require3.emitCommonTypes, + emitDictionary = _require3.emitDictionary, + emitFunction = _require3.emitFunction, + emitPromise = _require3.emitPromise, + emitRootTag = _require3.emitRootTag, + emitUnion = _require3.emitUnion, + typeAliasResolution = _require3.typeAliasResolution, + typeEnumResolution = _require3.typeEnumResolution; +function translateTypeAnnotation( + hasteModuleName, + /** + * TODO(T71778680): Flow-type this node. + */ + flowTypeAnnotation, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + parser, +) { + const resolveTypeAnnotationFN = parser.getResolveTypeAnnotationFN(); + const _resolveTypeAnnotatio = resolveTypeAnnotationFN( + flowTypeAnnotation, + types, + parser, + ), + nullable = _resolveTypeAnnotatio.nullable, + typeAnnotation = _resolveTypeAnnotatio.typeAnnotation, + typeResolutionStatus = _resolveTypeAnnotatio.typeResolutionStatus; + switch (typeAnnotation.type) { + case 'GenericTypeAnnotation': { + switch (parser.getTypeAnnotationName(typeAnnotation)) { + case 'RootTag': { + return emitRootTag(nullable); + } + case 'Promise': { + return emitPromise( + hasteModuleName, + typeAnnotation, + parser, + nullable, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + translateTypeAnnotation, + ); + } + case 'Array': + case '$ReadOnlyArray': { + return emitArrayType( + hasteModuleName, + typeAnnotation, + parser, + types, + aliasMap, + enumMap, + cxxOnly, + nullable, + translateTypeAnnotation, + ); + } + case '$ReadOnly': { + assertGenericTypeAnnotationHasExactlyOneTypeParameter( + hasteModuleName, + typeAnnotation, + parser, + ); + const _unwrapNullable = unwrapNullable( + translateTypeAnnotation( + hasteModuleName, + typeAnnotation.typeParameters.params[0], + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + parser, + ), + ), + _unwrapNullable2 = _slicedToArray(_unwrapNullable, 2), + paramType = _unwrapNullable2[0], + isParamNullable = _unwrapNullable2[1]; + return wrapNullable(nullable || isParamNullable, paramType); + } + default: { + const commonType = emitCommonTypes( + hasteModuleName, + types, + typeAnnotation, + aliasMap, + enumMap, + tryParse, + cxxOnly, + nullable, + parser, + ); + if (!commonType) { + throw new UnsupportedGenericParserError( + hasteModuleName, + typeAnnotation, + parser, + ); + } + return commonType; + } + } + } + case 'ObjectTypeAnnotation': { + // if there is any indexer, then it is a dictionary + if (typeAnnotation.indexers) { + const indexers = typeAnnotation.indexers.filter( + member => member.type === 'ObjectTypeIndexer', + ); + if (indexers.length > 0) { + // check the property type to prevent developers from using unsupported types + // the return value from `translateTypeAnnotation` is unused + const propertyType = indexers[0].value; + const valueType = translateTypeAnnotation( + hasteModuleName, + propertyType, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + parser, + ); + // no need to do further checking + return emitDictionary(nullable, valueType); + } + } + const objectTypeAnnotation = { + type: 'ObjectTypeAnnotation', + // $FlowFixMe[missing-type-arg] + properties: [...typeAnnotation.properties, ...typeAnnotation.indexers] + .map(property => { + return tryParse(() => { + return parseObjectProperty( + flowTypeAnnotation, + property, + hasteModuleName, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + nullable, + translateTypeAnnotation, + parser, + ); + }); + }) + .filter(Boolean), + }; + return typeAliasResolution( + typeResolutionStatus, + objectTypeAnnotation, + aliasMap, + nullable, + ); + } + case 'FunctionTypeAnnotation': { + return emitFunction( + nullable, + hasteModuleName, + typeAnnotation, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + translateTypeAnnotation, + parser, + ); + } + case 'UnionTypeAnnotation': { + return emitUnion(nullable, hasteModuleName, typeAnnotation, parser); + } + case 'StringLiteralTypeAnnotation': { + // 'a' is a special case for 'a' | 'b' but the type name is different + return wrapNullable(nullable, { + type: 'UnionTypeAnnotation', + memberType: 'StringTypeAnnotation', + }); + } + case 'EnumStringBody': + case 'EnumNumberBody': { + if ( + typeAnnotation.type === 'EnumNumberBody' && + typeAnnotation.members.some(m => { + var _m$init; + return ( + m.type === 'EnumNumberMember' && + !Number.isInteger( + (_m$init = m.init) === null || _m$init === void 0 + ? void 0 + : _m$init.value, + ) + ); + }) + ) { + throw new UnsupportedEnumDeclarationParserError( + hasteModuleName, + typeAnnotation, + parser.language(), + ); + } + return typeEnumResolution( + typeAnnotation, + typeResolutionStatus, + nullable, + hasteModuleName, + enumMap, + parser, + ); + } + default: { + const commonType = emitCommonTypes( + hasteModuleName, + types, + typeAnnotation, + aliasMap, + enumMap, + tryParse, + cxxOnly, + nullable, + parser, + ); + if (!commonType) { + throw new UnsupportedTypeAnnotationParserError( + hasteModuleName, + typeAnnotation, + parser.language(), + ); + } + return commonType; + } + } +} +module.exports = { + flowTranslateTypeAnnotation: translateTypeAnnotation, +}; diff --git a/packages/react-native-codegen/lib/parsers/flow/modules/index.js.flow b/packages/react-native-codegen/lib/parsers/flow/modules/index.js.flow new file mode 100644 index 00000000000000..d08345353a3a90 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/flow/modules/index.js.flow @@ -0,0 +1,280 @@ +/** + * 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 { + NamedShape, + NativeModuleAliasMap, + NativeModuleBaseTypeAnnotation, + NativeModuleEnumMap, + NativeModuleTypeAnnotation, + Nullable, +} from '../../../CodegenSchema'; +import type {Parser} from '../../parser'; +import type {ParserErrorCapturer, TypeDeclarationMap} from '../../utils'; + +const { + UnsupportedEnumDeclarationParserError, + UnsupportedGenericParserError, + UnsupportedTypeAnnotationParserError, +} = require('../../errors'); +const { + assertGenericTypeAnnotationHasExactlyOneTypeParameter, + parseObjectProperty, + unwrapNullable, + wrapNullable, +} = require('../../parsers-commons'); +const { + emitArrayType, + emitCommonTypes, + emitDictionary, + emitFunction, + emitPromise, + emitRootTag, + emitUnion, + typeAliasResolution, + typeEnumResolution, +} = require('../../parsers-primitives'); + +function translateTypeAnnotation( + hasteModuleName: string, + /** + * TODO(T71778680): Flow-type this node. + */ + flowTypeAnnotation: $FlowFixMe, + types: TypeDeclarationMap, + aliasMap: {...NativeModuleAliasMap}, + enumMap: {...NativeModuleEnumMap}, + tryParse: ParserErrorCapturer, + cxxOnly: boolean, + parser: Parser, +): Nullable { + const resolveTypeAnnotationFN = parser.getResolveTypeAnnotationFN(); + const {nullable, typeAnnotation, typeResolutionStatus} = + resolveTypeAnnotationFN(flowTypeAnnotation, types, parser); + + switch (typeAnnotation.type) { + case 'GenericTypeAnnotation': { + switch (parser.getTypeAnnotationName(typeAnnotation)) { + case 'RootTag': { + return emitRootTag(nullable); + } + case 'Promise': { + return emitPromise( + hasteModuleName, + typeAnnotation, + parser, + nullable, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + translateTypeAnnotation, + ); + } + case 'Array': + case '$ReadOnlyArray': { + return emitArrayType( + hasteModuleName, + typeAnnotation, + parser, + types, + aliasMap, + enumMap, + cxxOnly, + nullable, + translateTypeAnnotation, + ); + } + case '$ReadOnly': { + assertGenericTypeAnnotationHasExactlyOneTypeParameter( + hasteModuleName, + typeAnnotation, + parser, + ); + + const [paramType, isParamNullable] = unwrapNullable( + translateTypeAnnotation( + hasteModuleName, + typeAnnotation.typeParameters.params[0], + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + parser, + ), + ); + + return wrapNullable(nullable || isParamNullable, paramType); + } + default: { + const commonType = emitCommonTypes( + hasteModuleName, + types, + typeAnnotation, + aliasMap, + enumMap, + tryParse, + cxxOnly, + nullable, + parser, + ); + + if (!commonType) { + throw new UnsupportedGenericParserError( + hasteModuleName, + typeAnnotation, + parser, + ); + } + return commonType; + } + } + } + case 'ObjectTypeAnnotation': { + // if there is any indexer, then it is a dictionary + if (typeAnnotation.indexers) { + const indexers = typeAnnotation.indexers.filter( + member => member.type === 'ObjectTypeIndexer', + ); + if (indexers.length > 0) { + // check the property type to prevent developers from using unsupported types + // the return value from `translateTypeAnnotation` is unused + const propertyType = indexers[0].value; + const valueType = translateTypeAnnotation( + hasteModuleName, + propertyType, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + parser, + ); + // no need to do further checking + return emitDictionary(nullable, valueType); + } + } + + const objectTypeAnnotation = { + type: 'ObjectTypeAnnotation', + // $FlowFixMe[missing-type-arg] + properties: ([ + ...typeAnnotation.properties, + ...typeAnnotation.indexers, + ]: Array<$FlowFixMe>) + .map>>( + property => { + return tryParse(() => { + return parseObjectProperty( + flowTypeAnnotation, + property, + hasteModuleName, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + nullable, + translateTypeAnnotation, + parser, + ); + }); + }, + ) + .filter(Boolean), + }; + + return typeAliasResolution( + typeResolutionStatus, + objectTypeAnnotation, + aliasMap, + nullable, + ); + } + case 'FunctionTypeAnnotation': { + return emitFunction( + nullable, + hasteModuleName, + typeAnnotation, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + translateTypeAnnotation, + parser, + ); + } + case 'UnionTypeAnnotation': { + return emitUnion(nullable, hasteModuleName, typeAnnotation, parser); + } + case 'StringLiteralTypeAnnotation': { + // 'a' is a special case for 'a' | 'b' but the type name is different + return wrapNullable(nullable, { + type: 'UnionTypeAnnotation', + memberType: 'StringTypeAnnotation', + }); + } + case 'EnumStringBody': + case 'EnumNumberBody': { + if ( + typeAnnotation.type === 'EnumNumberBody' && + typeAnnotation.members.some( + m => + m.type === 'EnumNumberMember' && !Number.isInteger(m.init?.value), + ) + ) { + throw new UnsupportedEnumDeclarationParserError( + hasteModuleName, + typeAnnotation, + parser.language(), + ); + } + return typeEnumResolution( + typeAnnotation, + typeResolutionStatus, + nullable, + hasteModuleName, + enumMap, + parser, + ); + } + default: { + const commonType = emitCommonTypes( + hasteModuleName, + types, + typeAnnotation, + aliasMap, + enumMap, + tryParse, + cxxOnly, + nullable, + parser, + ); + + if (!commonType) { + throw new UnsupportedTypeAnnotationParserError( + hasteModuleName, + typeAnnotation, + parser.language(), + ); + } + return commonType; + } + } +} + +module.exports = { + flowTranslateTypeAnnotation: translateTypeAnnotation, +}; diff --git a/packages/react-native-codegen/lib/parsers/flow/parseFlowAndThrowErrors.js b/packages/react-native-codegen/lib/parsers/flow/parseFlowAndThrowErrors.js new file mode 100644 index 00000000000000..9a995eb6536051 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/flow/parseFlowAndThrowErrors.js @@ -0,0 +1,97 @@ +/** + * 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'; + +function ownKeys(e, r) { + var t = Object.keys(e); + if (Object.getOwnPropertySymbols) { + var o = Object.getOwnPropertySymbols(e); + r && + (o = o.filter(function (r) { + return Object.getOwnPropertyDescriptor(e, r).enumerable; + })), + t.push.apply(t, o); + } + return t; +} +function _objectSpread(e) { + for (var r = 1; r < arguments.length; r++) { + var t = null != arguments[r] ? arguments[r] : {}; + r % 2 + ? ownKeys(Object(t), !0).forEach(function (r) { + _defineProperty(e, r, t[r]); + }) + : Object.getOwnPropertyDescriptors + ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) + : ownKeys(Object(t)).forEach(function (r) { + Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); + }); + } + return e; +} +function _defineProperty(e, r, t) { + return ( + (r = _toPropertyKey(r)) in e + ? Object.defineProperty(e, r, { + value: t, + enumerable: !0, + configurable: !0, + writable: !0, + }) + : (e[r] = t), + e + ); +} +function _toPropertyKey(t) { + var i = _toPrimitive(t, 'string'); + return 'symbol' == typeof i ? i : i + ''; +} +function _toPrimitive(t, r) { + if ('object' != typeof t || !t) return t; + var e = t[Symbol.toPrimitive]; + if (void 0 !== e) { + var i = e.call(t, r || 'default'); + if ('object' != typeof i) return i; + throw new TypeError('@@toPrimitive must return a primitive value.'); + } + return ('string' === r ? String : Number)(t); +} +const hermesParser = require('hermes-parser'); +function parseFlowAndThrowErrors(code, options = {}) { + let ast; + try { + ast = hermesParser.parse( + code, + _objectSpread( + { + // Produce an ESTree-compliant AST + babel: false, + // Parse Flow without a pragma + flow: 'all', + }, + options.filename != null + ? { + sourceFilename: options.filename, + } + : {}, + ), + ); + } catch (e) { + if (options.filename != null) { + e.message = `Syntax error in ${options.filename}: ${e.message}`; + } + throw e; + } + return ast; +} +module.exports = { + parseFlowAndThrowErrors, +}; diff --git a/packages/react-native-codegen/lib/parsers/flow/parseFlowAndThrowErrors.js.flow b/packages/react-native-codegen/lib/parsers/flow/parseFlowAndThrowErrors.js.flow new file mode 100644 index 00000000000000..8ffb03422e4fc0 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/flow/parseFlowAndThrowErrors.js.flow @@ -0,0 +1,41 @@ +/** + * 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 {Program as ESTreeProgram} from 'hermes-estree'; + +const hermesParser = require('hermes-parser'); + +function parseFlowAndThrowErrors( + code: string, + options: $ReadOnly<{filename?: ?string}> = {}, +): ESTreeProgram { + let ast; + try { + ast = hermesParser.parse(code, { + // Produce an ESTree-compliant AST + babel: false, + // Parse Flow without a pragma + flow: 'all', + ...(options.filename != null ? {sourceFilename: options.filename} : {}), + }); + } catch (e) { + if (options.filename != null) { + e.message = `Syntax error in ${options.filename}: ${e.message}`; + } + throw e; + } + return ast; +} + +module.exports = { + parseFlowAndThrowErrors, +}; diff --git a/packages/react-native-codegen/lib/parsers/flow/parser.d.ts b/packages/react-native-codegen/lib/parsers/flow/parser.d.ts new file mode 100644 index 00000000000000..2a60b3683df066 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/flow/parser.d.ts @@ -0,0 +1,17 @@ +/** + * 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. + */ + +import type { Parser } from '../parser'; +import type { SchemaType } from '../../CodegenSchema'; +import type { ParserType } from '../errors'; + +export declare class FlowParser implements Parser { + language(): ParserType; + parseFile(filename: string): SchemaType; + parseString(contents: string, filename?: string): SchemaType; + parseModuleFixture(filename: string): SchemaType; +} diff --git a/packages/react-native-codegen/lib/parsers/flow/parser.js b/packages/react-native-codegen/lib/parsers/flow/parser.js new file mode 100644 index 00000000000000..5d3e0a8346074b --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/flow/parser.js @@ -0,0 +1,480 @@ +/** + * 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'; + +function _defineProperty(e, r, t) { + return ( + (r = _toPropertyKey(r)) in e + ? Object.defineProperty(e, r, { + value: t, + enumerable: !0, + configurable: !0, + writable: !0, + }) + : (e[r] = t), + e + ); +} +function _toPropertyKey(t) { + var i = _toPrimitive(t, 'string'); + return 'symbol' == typeof i ? i : i + ''; +} +function _toPrimitive(t, r) { + if ('object' != typeof t || !t) return t; + var e = t[Symbol.toPrimitive]; + if (void 0 !== e) { + var i = e.call(t, r || 'default'); + if ('object' != typeof i) return i; + throw new TypeError('@@toPrimitive must return a primitive value.'); + } + return ('string' === r ? String : Number)(t); +} +const _require = require('../errors'), + UnsupportedObjectPropertyTypeAnnotationParserError = + _require.UnsupportedObjectPropertyTypeAnnotationParserError; +const _require2 = require('../parsers-commons'), + buildModuleSchema = _require2.buildModuleSchema, + buildPropSchema = _require2.buildPropSchema, + buildSchema = _require2.buildSchema, + handleGenericTypeAnnotation = _require2.handleGenericTypeAnnotation; +const _require3 = require('../parsers-primitives'), + Visitor = _require3.Visitor; +const _require4 = require('../schema.js'), + wrapComponentSchema = _require4.wrapComponentSchema; +const _require5 = require('./components'), + buildComponentSchema = _require5.buildComponentSchema; +const _require6 = require('./components/componentsUtils'), + flattenProperties = _require6.flattenProperties, + getSchemaInfo = _require6.getSchemaInfo, + getTypeAnnotation = _require6.getTypeAnnotation; +const _require7 = require('./modules'), + flowTranslateTypeAnnotation = _require7.flowTranslateTypeAnnotation; +const _require8 = require('./parseFlowAndThrowErrors'), + parseFlowAndThrowErrors = _require8.parseFlowAndThrowErrors; +const fs = require('fs'); +const invariant = require('invariant'); +class FlowParser { + constructor() { + _defineProperty( + this, + 'typeParameterInstantiation', + 'TypeParameterInstantiation', + ); + _defineProperty(this, 'typeAlias', 'TypeAlias'); + _defineProperty(this, 'enumDeclaration', 'EnumDeclaration'); + _defineProperty(this, 'interfaceDeclaration', 'InterfaceDeclaration'); + _defineProperty( + this, + 'nullLiteralTypeAnnotation', + 'NullLiteralTypeAnnotation', + ); + _defineProperty( + this, + 'undefinedLiteralTypeAnnotation', + 'VoidLiteralTypeAnnotation', + ); + } + isProperty(property) { + return property.type === 'ObjectTypeProperty'; + } + getKeyName(property, hasteModuleName) { + if (!this.isProperty(property)) { + throw new UnsupportedObjectPropertyTypeAnnotationParserError( + hasteModuleName, + property, + property.type, + this.language(), + ); + } + return property.key.name; + } + language() { + return 'Flow'; + } + getTypeAnnotationName(typeAnnotation) { + var _typeAnnotation$id; + return typeAnnotation === null || + typeAnnotation === void 0 || + (_typeAnnotation$id = typeAnnotation.id) === null || + _typeAnnotation$id === void 0 + ? void 0 + : _typeAnnotation$id.name; + } + checkIfInvalidModule(typeArguments) { + return ( + typeArguments.type !== 'TypeParameterInstantiation' || + typeArguments.params.length !== 1 || + typeArguments.params[0].type !== 'GenericTypeAnnotation' || + typeArguments.params[0].id.name !== 'Spec' + ); + } + remapUnionTypeAnnotationMemberNames(membersTypes) { + const remapLiteral = item => { + return item.type + .replace('NumberLiteralTypeAnnotation', 'NumberTypeAnnotation') + .replace('StringLiteralTypeAnnotation', 'StringTypeAnnotation'); + }; + return [...new Set(membersTypes.map(remapLiteral))]; + } + parseFile(filename) { + const contents = fs.readFileSync(filename, 'utf8'); + return this.parseString(contents, filename); + } + parseString(contents, filename) { + return buildSchema( + contents, + filename, + wrapComponentSchema, + buildComponentSchema, + buildModuleSchema, + Visitor, + this, + flowTranslateTypeAnnotation, + ); + } + parseModuleFixture(filename) { + const contents = fs.readFileSync(filename, 'utf8'); + return this.parseString(contents, 'path/NativeSampleTurboModule.js'); + } + getAst(contents, filename) { + return parseFlowAndThrowErrors(contents, { + filename, + }); + } + getFunctionTypeAnnotationParameters(functionTypeAnnotation) { + return functionTypeAnnotation.params; + } + getFunctionNameFromParameter(parameter) { + return parameter.name; + } + getParameterName(parameter) { + return parameter.name.name; + } + getParameterTypeAnnotation(parameter) { + return parameter.typeAnnotation; + } + getFunctionTypeAnnotationReturnType(functionTypeAnnotation) { + return functionTypeAnnotation.returnType; + } + parseEnumMembersType(typeAnnotation) { + const enumMembersType = + typeAnnotation.type === 'EnumStringBody' + ? 'StringTypeAnnotation' + : typeAnnotation.type === 'EnumNumberBody' + ? 'NumberTypeAnnotation' + : null; + if (!enumMembersType) { + throw new Error( + `Unknown enum type annotation type. Got: ${typeAnnotation.type}. Expected: EnumStringBody or EnumNumberBody.`, + ); + } + return enumMembersType; + } + validateEnumMembersSupported(typeAnnotation, enumMembersType) { + if (!typeAnnotation.members || typeAnnotation.members.length === 0) { + // passing mixed members to flow would result in a flow error + // if the tool is launched ignoring that error, the enum would appear like not having enums + throw new Error( + 'Enums should have at least one member and member values can not be mixed- they all must be either blank, number, or string values.', + ); + } + typeAnnotation.members.forEach(member => { + if ( + enumMembersType === 'StringTypeAnnotation' && + (!member.init || typeof member.init.value === 'string') + ) { + return; + } + if ( + enumMembersType === 'NumberTypeAnnotation' && + member.init && + typeof member.init.value === 'number' + ) { + return; + } + throw new Error( + 'Enums can not be mixed- they all must be either blank, number, or string values.', + ); + }); + } + parseEnumMembers(typeAnnotation) { + return typeAnnotation.members.map(member => { + var _member$init$value, _member$init; + return { + name: member.id.name, + value: + (_member$init$value = + (_member$init = member.init) === null || _member$init === void 0 + ? void 0 + : _member$init.value) !== null && _member$init$value !== void 0 + ? _member$init$value + : member.id.name, + }; + }); + } + isModuleInterface(node) { + return ( + node.type === 'InterfaceDeclaration' && + node.extends.length === 1 && + node.extends[0].type === 'InterfaceExtends' && + node.extends[0].id.name === 'TurboModule' + ); + } + isGenericTypeAnnotation(type) { + return type === 'GenericTypeAnnotation'; + } + extractAnnotatedElement(typeAnnotation, types) { + return types[typeAnnotation.typeParameters.params[0].id.name]; + } + + /** + * This FlowFixMe is supposed to refer to an InterfaceDeclaration or TypeAlias + * declaration type. Unfortunately, we don't have those types, because flow-parser + * generates them, and flow-parser is not type-safe. In the future, we should find + * a way to get these types from our flow parser library. + * + * TODO(T71778680): Flow type AST Nodes + */ + + getTypes(ast) { + return ast.body.reduce((types, node) => { + if ( + node.type === 'ExportNamedDeclaration' && + node.exportKind === 'type' + ) { + if ( + node.declaration != null && + (node.declaration.type === 'TypeAlias' || + node.declaration.type === 'InterfaceDeclaration') + ) { + types[node.declaration.id.name] = node.declaration; + } + } else if ( + node.type === 'ExportNamedDeclaration' && + node.exportKind === 'value' && + node.declaration && + node.declaration.type === 'EnumDeclaration' + ) { + types[node.declaration.id.name] = node.declaration; + } else if ( + node.type === 'TypeAlias' || + node.type === 'InterfaceDeclaration' || + node.type === 'EnumDeclaration' + ) { + types[node.id.name] = node; + } + return types; + }, {}); + } + callExpressionTypeParameters(callExpression) { + return callExpression.typeArguments || null; + } + computePartialProperties( + properties, + hasteModuleName, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + ) { + return properties.map(prop => { + return { + name: prop.key.name, + optional: true, + typeAnnotation: flowTranslateTypeAnnotation( + hasteModuleName, + prop.value, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + this, + ), + }; + }); + } + functionTypeAnnotation(propertyValueType) { + return propertyValueType === 'FunctionTypeAnnotation'; + } + getTypeArgumentParamsFromDeclaration(declaration) { + return declaration.typeArguments.params; + } + + /** + * This FlowFixMe is supposed to refer to typeArgumentParams and + * funcArgumentParams of generated AST. + */ + getNativeComponentType(typeArgumentParams, funcArgumentParams) { + return { + propsTypeName: typeArgumentParams[0].id.name, + componentName: funcArgumentParams[0].value, + }; + } + getAnnotatedElementProperties(annotatedElement) { + return annotatedElement.right.properties; + } + bodyProperties(typeAlias) { + return typeAlias.body.properties; + } + convertKeywordToTypeAnnotation(keyword) { + return keyword; + } + argumentForProp(prop) { + return prop.argument; + } + nameForArgument(prop) { + return prop.argument.id.name; + } + isOptionalProperty(property) { + return ( + property.value.type === 'NullableTypeAnnotation' || property.optional + ); + } + getGetSchemaInfoFN() { + return getSchemaInfo; + } + getTypeAnnotationFromProperty(property) { + return property.value.type === 'NullableTypeAnnotation' + ? property.value.typeAnnotation + : property.value; + } + getGetTypeAnnotationFN() { + return getTypeAnnotation; + } + getResolvedTypeAnnotation(typeAnnotation, types, parser) { + invariant( + typeAnnotation != null, + 'resolveTypeAnnotation(): typeAnnotation cannot be null', + ); + let node = typeAnnotation; + let nullable = false; + let typeResolutionStatus = { + successful: false, + }; + for (;;) { + if (node.type === 'NullableTypeAnnotation') { + nullable = true; + node = node.typeAnnotation; + continue; + } + if (node.type !== 'GenericTypeAnnotation') { + break; + } + const typeAnnotationName = this.getTypeAnnotationName(node); + const resolvedTypeAnnotation = types[typeAnnotationName]; + if (resolvedTypeAnnotation == null) { + break; + } + const _handleGenericTypeAnn = handleGenericTypeAnnotation( + node, + resolvedTypeAnnotation, + this, + ), + typeAnnotationNode = _handleGenericTypeAnn.typeAnnotation, + status = _handleGenericTypeAnn.typeResolutionStatus; + typeResolutionStatus = status; + node = typeAnnotationNode; + } + return { + nullable: nullable, + typeAnnotation: node, + typeResolutionStatus, + }; + } + getResolveTypeAnnotationFN() { + return (typeAnnotation, types, parser) => + this.getResolvedTypeAnnotation(typeAnnotation, types, parser); + } + extendsForProp(prop, types, parser) { + const argument = this.argumentForProp(prop); + if (!argument) { + console.log('null', prop); + } + const name = parser.nameForArgument(prop); + 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}`); + } + } + } + removeKnownExtends(typeDefinition, types) { + return typeDefinition.filter( + prop => + prop.type !== 'ObjectTypeSpreadProperty' || + this.extendsForProp(prop, types, this) === null, + ); + } + getExtendsProps(typeDefinition, types) { + return typeDefinition + .filter(prop => prop.type === 'ObjectTypeSpreadProperty') + .map(prop => this.extendsForProp(prop, types, this)) + .filter(Boolean); + } + getProps(typeDefinition, types) { + const nonExtendsProps = this.removeKnownExtends(typeDefinition, types); + const props = flattenProperties(nonExtendsProps, types, this) + .map(property => buildPropSchema(property, types, this)) + .filter(Boolean); + return { + props, + extendsProps: this.getExtendsProps(typeDefinition, types), + }; + } + getProperties(typeName, types) { + const typeAlias = types[typeName]; + try { + return typeAlias.right.typeParameters.params[0].properties; + } catch (e) { + throw new Error( + `Failed to find type definition for "${typeName}", please check that you have a valid codegen flow file`, + ); + } + } + nextNodeForTypeAlias(typeAnnotation) { + return typeAnnotation.right; + } + nextNodeForEnum(typeAnnotation) { + return typeAnnotation.body; + } + genericTypeAnnotationErrorMessage(typeAnnotation) { + return `A non GenericTypeAnnotation must be a type declaration ('${this.typeAlias}') or enum ('${this.enumDeclaration}'). Instead, got the unsupported ${typeAnnotation.type}.`; + } + extractTypeFromTypeAnnotation(typeAnnotation) { + return typeAnnotation.type === 'GenericTypeAnnotation' + ? typeAnnotation.id.name + : typeAnnotation.type; + } + getObjectProperties(typeAnnotation) { + return typeAnnotation.properties; + } + getLiteralValue(option) { + return option.value; + } + getPaperTopLevelNameDeprecated(typeAnnotation) { + return typeAnnotation.typeParameters.params.length > 1 + ? typeAnnotation.typeParameters.params[1].value + : null; + } +} +module.exports = { + FlowParser, +}; diff --git a/packages/react-native-codegen/lib/parsers/flow/parser.js.flow b/packages/react-native-codegen/lib/parsers/flow/parser.js.flow new file mode 100644 index 00000000000000..91cac322b93b4e --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/flow/parser.js.flow @@ -0,0 +1,551 @@ +/** + * 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 { + ExtendsPropsShape, + NamedShape, + NativeModuleAliasMap, + NativeModuleEnumMap, + NativeModuleEnumMembers, + NativeModuleEnumMemberType, + NativeModuleParamTypeAnnotation, + Nullable, + PropTypeAnnotation, + SchemaType, + UnionTypeAnnotationMemberType, +} from '../../CodegenSchema'; +import type {ParserType} from '../errors'; +import type { + GetSchemaInfoFN, + GetTypeAnnotationFN, + Parser, + ResolveTypeAnnotationFN, +} from '../parser'; +import type { + ParserErrorCapturer, + PropAST, + TypeDeclarationMap, + TypeResolutionStatus, +} from '../utils'; + +const { + UnsupportedObjectPropertyTypeAnnotationParserError, +} = require('../errors'); +const { + buildModuleSchema, + buildPropSchema, + buildSchema, + handleGenericTypeAnnotation, +} = require('../parsers-commons'); +const {Visitor} = require('../parsers-primitives'); +const {wrapComponentSchema} = require('../schema.js'); +const {buildComponentSchema} = require('./components'); +const { + flattenProperties, + getSchemaInfo, + getTypeAnnotation, +} = require('./components/componentsUtils'); +const {flowTranslateTypeAnnotation} = require('./modules'); +const {parseFlowAndThrowErrors} = require('./parseFlowAndThrowErrors'); +const fs = require('fs'); +const invariant = require('invariant'); + +type ExtendsForProp = null | { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', +}; + +class FlowParser implements Parser { + typeParameterInstantiation: string = 'TypeParameterInstantiation'; + typeAlias: string = 'TypeAlias'; + enumDeclaration: string = 'EnumDeclaration'; + interfaceDeclaration: string = 'InterfaceDeclaration'; + nullLiteralTypeAnnotation: string = 'NullLiteralTypeAnnotation'; + undefinedLiteralTypeAnnotation: string = 'VoidLiteralTypeAnnotation'; + + isProperty(property: $FlowFixMe): boolean { + return property.type === 'ObjectTypeProperty'; + } + + getKeyName(property: $FlowFixMe, hasteModuleName: string): string { + if (!this.isProperty(property)) { + throw new UnsupportedObjectPropertyTypeAnnotationParserError( + hasteModuleName, + property, + property.type, + this.language(), + ); + } + return property.key.name; + } + + language(): ParserType { + return 'Flow'; + } + + getTypeAnnotationName(typeAnnotation: $FlowFixMe): string { + return typeAnnotation?.id?.name; + } + + checkIfInvalidModule(typeArguments: $FlowFixMe): boolean { + return ( + typeArguments.type !== 'TypeParameterInstantiation' || + typeArguments.params.length !== 1 || + typeArguments.params[0].type !== 'GenericTypeAnnotation' || + typeArguments.params[0].id.name !== 'Spec' + ); + } + + remapUnionTypeAnnotationMemberNames( + membersTypes: $FlowFixMe[], + ): UnionTypeAnnotationMemberType[] { + const remapLiteral = (item: $FlowFixMe) => { + return item.type + .replace('NumberLiteralTypeAnnotation', 'NumberTypeAnnotation') + .replace('StringLiteralTypeAnnotation', 'StringTypeAnnotation'); + }; + + return [...new Set(membersTypes.map(remapLiteral))]; + } + + parseFile(filename: string): SchemaType { + const contents = fs.readFileSync(filename, 'utf8'); + + return this.parseString(contents, filename); + } + + parseString(contents: string, filename: ?string): SchemaType { + return buildSchema( + contents, + filename, + wrapComponentSchema, + buildComponentSchema, + buildModuleSchema, + Visitor, + this, + flowTranslateTypeAnnotation, + ); + } + + parseModuleFixture(filename: string): SchemaType { + const contents = fs.readFileSync(filename, 'utf8'); + + return this.parseString(contents, 'path/NativeSampleTurboModule.js'); + } + + getAst(contents: string, filename?: ?string): $FlowFixMe { + return parseFlowAndThrowErrors(contents, {filename}); + } + + getFunctionTypeAnnotationParameters( + functionTypeAnnotation: $FlowFixMe, + ): $ReadOnlyArray<$FlowFixMe> { + return functionTypeAnnotation.params; + } + + getFunctionNameFromParameter( + parameter: NamedShape>, + ): $FlowFixMe { + return parameter.name; + } + + getParameterName(parameter: $FlowFixMe): string { + return parameter.name.name; + } + + getParameterTypeAnnotation(parameter: $FlowFixMe): $FlowFixMe { + return parameter.typeAnnotation; + } + + getFunctionTypeAnnotationReturnType( + functionTypeAnnotation: $FlowFixMe, + ): $FlowFixMe { + return functionTypeAnnotation.returnType; + } + + parseEnumMembersType(typeAnnotation: $FlowFixMe): NativeModuleEnumMemberType { + const enumMembersType: ?NativeModuleEnumMemberType = + typeAnnotation.type === 'EnumStringBody' + ? 'StringTypeAnnotation' + : typeAnnotation.type === 'EnumNumberBody' + ? 'NumberTypeAnnotation' + : null; + if (!enumMembersType) { + throw new Error( + `Unknown enum type annotation type. Got: ${typeAnnotation.type}. Expected: EnumStringBody or EnumNumberBody.`, + ); + } + return enumMembersType; + } + + validateEnumMembersSupported( + typeAnnotation: $FlowFixMe, + enumMembersType: NativeModuleEnumMemberType, + ): void { + if (!typeAnnotation.members || typeAnnotation.members.length === 0) { + // passing mixed members to flow would result in a flow error + // if the tool is launched ignoring that error, the enum would appear like not having enums + throw new Error( + 'Enums should have at least one member and member values can not be mixed- they all must be either blank, number, or string values.', + ); + } + + typeAnnotation.members.forEach(member => { + if ( + enumMembersType === 'StringTypeAnnotation' && + (!member.init || typeof member.init.value === 'string') + ) { + return; + } + + if ( + enumMembersType === 'NumberTypeAnnotation' && + member.init && + typeof member.init.value === 'number' + ) { + return; + } + + throw new Error( + 'Enums can not be mixed- they all must be either blank, number, or string values.', + ); + }); + } + + parseEnumMembers(typeAnnotation: $FlowFixMe): NativeModuleEnumMembers { + return typeAnnotation.members.map(member => ({ + name: member.id.name, + value: member.init?.value ?? member.id.name, + })); + } + + isModuleInterface(node: $FlowFixMe): boolean { + return ( + node.type === 'InterfaceDeclaration' && + node.extends.length === 1 && + node.extends[0].type === 'InterfaceExtends' && + node.extends[0].id.name === 'TurboModule' + ); + } + + isGenericTypeAnnotation(type: $FlowFixMe): boolean { + return type === 'GenericTypeAnnotation'; + } + + extractAnnotatedElement( + typeAnnotation: $FlowFixMe, + types: TypeDeclarationMap, + ): $FlowFixMe { + return types[typeAnnotation.typeParameters.params[0].id.name]; + } + + /** + * This FlowFixMe is supposed to refer to an InterfaceDeclaration or TypeAlias + * declaration type. Unfortunately, we don't have those types, because flow-parser + * generates them, and flow-parser is not type-safe. In the future, we should find + * a way to get these types from our flow parser library. + * + * TODO(T71778680): Flow type AST Nodes + */ + + getTypes(ast: $FlowFixMe): TypeDeclarationMap { + return ast.body.reduce((types, node) => { + if ( + node.type === 'ExportNamedDeclaration' && + node.exportKind === 'type' + ) { + if ( + node.declaration != null && + (node.declaration.type === 'TypeAlias' || + node.declaration.type === 'InterfaceDeclaration') + ) { + types[node.declaration.id.name] = node.declaration; + } + } else if ( + node.type === 'ExportNamedDeclaration' && + node.exportKind === 'value' && + node.declaration && + node.declaration.type === 'EnumDeclaration' + ) { + types[node.declaration.id.name] = node.declaration; + } else if ( + node.type === 'TypeAlias' || + node.type === 'InterfaceDeclaration' || + node.type === 'EnumDeclaration' + ) { + types[node.id.name] = node; + } + return types; + }, {}); + } + + callExpressionTypeParameters(callExpression: $FlowFixMe): $FlowFixMe | null { + return callExpression.typeArguments || null; + } + + computePartialProperties( + properties: Array<$FlowFixMe>, + hasteModuleName: string, + types: TypeDeclarationMap, + aliasMap: {...NativeModuleAliasMap}, + enumMap: {...NativeModuleEnumMap}, + tryParse: ParserErrorCapturer, + cxxOnly: boolean, + ): Array<$FlowFixMe> { + return properties.map(prop => { + return { + name: prop.key.name, + optional: true, + typeAnnotation: flowTranslateTypeAnnotation( + hasteModuleName, + prop.value, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + this, + ), + }; + }); + } + + functionTypeAnnotation(propertyValueType: string): boolean { + return propertyValueType === 'FunctionTypeAnnotation'; + } + + getTypeArgumentParamsFromDeclaration(declaration: $FlowFixMe): $FlowFixMe { + return declaration.typeArguments.params; + } + + /** + * This FlowFixMe is supposed to refer to typeArgumentParams and + * funcArgumentParams of generated AST. + */ + getNativeComponentType( + typeArgumentParams: $FlowFixMe, + funcArgumentParams: $FlowFixMe, + ): {[string]: string} { + return { + propsTypeName: typeArgumentParams[0].id.name, + componentName: funcArgumentParams[0].value, + }; + } + + getAnnotatedElementProperties(annotatedElement: $FlowFixMe): $FlowFixMe { + return annotatedElement.right.properties; + } + + bodyProperties(typeAlias: $FlowFixMe): $ReadOnlyArray<$FlowFixMe> { + return typeAlias.body.properties; + } + + convertKeywordToTypeAnnotation(keyword: string): string { + return keyword; + } + + argumentForProp(prop: PropAST): $FlowFixMe { + return prop.argument; + } + + nameForArgument(prop: PropAST): $FlowFixMe { + return prop.argument.id.name; + } + + isOptionalProperty(property: $FlowFixMe): boolean { + return ( + property.value.type === 'NullableTypeAnnotation' || property.optional + ); + } + + getGetSchemaInfoFN(): GetSchemaInfoFN { + return getSchemaInfo; + } + + getTypeAnnotationFromProperty(property: PropAST): $FlowFixMe { + return property.value.type === 'NullableTypeAnnotation' + ? property.value.typeAnnotation + : property.value; + } + + getGetTypeAnnotationFN(): GetTypeAnnotationFN { + return getTypeAnnotation; + } + + getResolvedTypeAnnotation( + typeAnnotation: $FlowFixMe, + types: TypeDeclarationMap, + parser: Parser, + ): { + nullable: boolean, + typeAnnotation: $FlowFixMe, + typeResolutionStatus: TypeResolutionStatus, + } { + invariant( + typeAnnotation != null, + 'resolveTypeAnnotation(): typeAnnotation cannot be null', + ); + + let node = typeAnnotation; + let nullable = false; + let typeResolutionStatus: TypeResolutionStatus = { + successful: false, + }; + + for (;;) { + if (node.type === 'NullableTypeAnnotation') { + nullable = true; + node = node.typeAnnotation; + continue; + } + + if (node.type !== 'GenericTypeAnnotation') { + break; + } + + const typeAnnotationName = this.getTypeAnnotationName(node); + const resolvedTypeAnnotation = types[typeAnnotationName]; + if (resolvedTypeAnnotation == null) { + break; + } + + const {typeAnnotation: typeAnnotationNode, typeResolutionStatus: status} = + handleGenericTypeAnnotation(node, resolvedTypeAnnotation, this); + typeResolutionStatus = status; + node = typeAnnotationNode; + } + + return { + nullable: nullable, + typeAnnotation: node, + typeResolutionStatus, + }; + } + + getResolveTypeAnnotationFN(): ResolveTypeAnnotationFN { + return (typeAnnotation, types, parser) => + this.getResolvedTypeAnnotation(typeAnnotation, types, parser); + } + extendsForProp( + prop: PropAST, + types: TypeDeclarationMap, + parser: Parser, + ): ExtendsForProp { + const argument = this.argumentForProp(prop); + if (!argument) { + console.log('null', prop); + } + const name = parser.nameForArgument(prop); + + 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}`); + } + } + } + + removeKnownExtends( + typeDefinition: $ReadOnlyArray, + types: TypeDeclarationMap, + ): $ReadOnlyArray { + return typeDefinition.filter( + prop => + prop.type !== 'ObjectTypeSpreadProperty' || + this.extendsForProp(prop, types, this) === null, + ); + } + + getExtendsProps( + typeDefinition: $ReadOnlyArray, + types: TypeDeclarationMap, + ): $ReadOnlyArray { + return typeDefinition + .filter(prop => prop.type === 'ObjectTypeSpreadProperty') + .map(prop => this.extendsForProp(prop, types, this)) + .filter(Boolean); + } + + getProps( + typeDefinition: $ReadOnlyArray, + types: TypeDeclarationMap, + ): { + props: $ReadOnlyArray>, + extendsProps: $ReadOnlyArray, + } { + const nonExtendsProps = this.removeKnownExtends(typeDefinition, types); + const props = flattenProperties(nonExtendsProps, types, this) + .map(property => buildPropSchema(property, types, this)) + .filter(Boolean); + + return { + props, + extendsProps: this.getExtendsProps(typeDefinition, types), + }; + } + + getProperties(typeName: string, types: TypeDeclarationMap): $FlowFixMe { + const typeAlias = types[typeName]; + try { + return typeAlias.right.typeParameters.params[0].properties; + } catch (e) { + throw new Error( + `Failed to find type definition for "${typeName}", please check that you have a valid codegen flow file`, + ); + } + } + + nextNodeForTypeAlias(typeAnnotation: $FlowFixMe): $FlowFixMe { + return typeAnnotation.right; + } + + nextNodeForEnum(typeAnnotation: $FlowFixMe): $FlowFixMe { + return typeAnnotation.body; + } + + genericTypeAnnotationErrorMessage(typeAnnotation: $FlowFixMe): string { + return `A non GenericTypeAnnotation must be a type declaration ('${this.typeAlias}') or enum ('${this.enumDeclaration}'). Instead, got the unsupported ${typeAnnotation.type}.`; + } + + extractTypeFromTypeAnnotation(typeAnnotation: $FlowFixMe): string { + return typeAnnotation.type === 'GenericTypeAnnotation' + ? typeAnnotation.id.name + : typeAnnotation.type; + } + + getObjectProperties(typeAnnotation: $FlowFixMe): $FlowFixMe { + return typeAnnotation.properties; + } + + getLiteralValue(option: $FlowFixMe): $FlowFixMe { + return option.value; + } + + getPaperTopLevelNameDeprecated(typeAnnotation: $FlowFixMe): $FlowFixMe { + return typeAnnotation.typeParameters.params.length > 1 + ? typeAnnotation.typeParameters.params[1].value + : null; + } +} + +module.exports = { + FlowParser, +}; diff --git a/packages/react-native-codegen/lib/parsers/flow/utils.js b/packages/react-native-codegen/lib/parsers/flow/utils.js new file mode 100644 index 00000000000000..31446ab11182c7 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/flow/utils.js @@ -0,0 +1,21 @@ +/** + * 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'; + +function getValueFromTypes(value, types) { + if (value.type === 'GenericTypeAnnotation' && types[value.id.name]) { + return getValueFromTypes(types[value.id.name].right, types); + } + return value; +} +module.exports = { + getValueFromTypes, +}; diff --git a/packages/react-native-codegen/lib/parsers/flow/utils.js.flow b/packages/react-native-codegen/lib/parsers/flow/utils.js.flow new file mode 100644 index 00000000000000..02af5916606827 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/flow/utils.js.flow @@ -0,0 +1,24 @@ +/** + * 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 {ASTNode, TypeDeclarationMap} from '../utils'; + +function getValueFromTypes(value: ASTNode, types: TypeDeclarationMap): ASTNode { + if (value.type === 'GenericTypeAnnotation' && types[value.id.name]) { + return getValueFromTypes(types[value.id.name].right, types); + } + return value; +} + +module.exports = { + getValueFromTypes, +}; diff --git a/packages/react-native-codegen/lib/parsers/parser.d.ts b/packages/react-native-codegen/lib/parsers/parser.d.ts new file mode 100644 index 00000000000000..ae6017a31832d6 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/parser.d.ts @@ -0,0 +1,17 @@ +/** + * 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. + */ + +import type { SchemaType } from '../CodegenSchema'; +import type { ParserType } from './errors'; + +// useful members only for downstream +export interface Parser { + language(): ParserType; + parseFile(filename: string): SchemaType; + parseString(contents: string, filename?: string): SchemaType; + parseModuleFixture(filename: string): SchemaType; +} diff --git a/packages/react-native-codegen/lib/parsers/parser.js b/packages/react-native-codegen/lib/parsers/parser.js new file mode 100644 index 00000000000000..4949d716cda2e2 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/parser.js @@ -0,0 +1,11 @@ +/** + * 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'; diff --git a/packages/react-native-codegen/lib/parsers/parser.js.flow b/packages/react-native-codegen/lib/parsers/parser.js.flow new file mode 100644 index 00000000000000..4c0d0257ed54ac --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/parser.js.flow @@ -0,0 +1,433 @@ +/** + * 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 { + ExtendsPropsShape, + NamedShape, + NativeModuleAliasMap, + NativeModuleEnumMap, + NativeModuleEnumMembers, + NativeModuleEnumMemberType, + NativeModuleParamTypeAnnotation, + Nullable, + PropTypeAnnotation, + SchemaType, + UnionTypeAnnotationMemberType, +} from '../CodegenSchema'; +import type {ParserType} from './errors'; +import type { + ASTNode, + ParserErrorCapturer, + PropAST, + TypeDeclarationMap, + TypeResolutionStatus, +} from './utils'; + +export type GetTypeAnnotationFN = ( + name: string, + annotation: $FlowFixMe | ASTNode, + defaultValue: $FlowFixMe | void, + withNullDefault: boolean, + types: TypeDeclarationMap, + parser: Parser, + buildSchema: ( + property: PropAST, + types: TypeDeclarationMap, + parser: Parser, + ) => $FlowFixMe, +) => $FlowFixMe; + +export type SchemaInfo = { + name: string, + optional: boolean, + typeAnnotation: $FlowFixMe, + defaultValue: $FlowFixMe, + withNullDefault: boolean, +}; + +export type GetSchemaInfoFN = ( + property: PropAST, + types: TypeDeclarationMap, +) => ?SchemaInfo; + +export type BuildSchemaFN = ( + property: PropAST, + types: TypeDeclarationMap, + parser: Parser, +) => ?NamedShape; + +export type ResolveTypeAnnotationFN = ( + typeAnnotation: $FlowFixMe, + types: TypeDeclarationMap, + parser: Parser, +) => { + nullable: boolean, + typeAnnotation: $FlowFixMe, + typeResolutionStatus: TypeResolutionStatus, +}; + +/** + * This is the main interface for Parsers of various languages. + * It exposes all the methods that contain language-specific logic. + */ +export interface Parser { + /** + * This is the TypeParameterInstantiation value + */ + typeParameterInstantiation: string; + + /** + * TypeAlias property of the Parser + */ + typeAlias: string; + + /** + * enumDeclaration Property of the Parser + */ + enumDeclaration: string; + + /** + * InterfaceDeclaration property of the Parser + */ + interfaceDeclaration: string; + + /** + * This is the NullLiteralTypeAnnotation value + */ + nullLiteralTypeAnnotation: string; + + /** + * UndefinedLiteralTypeAnnotation property of the Parser + */ + undefinedLiteralTypeAnnotation: string; + + /** + * Given a declaration, it returns true if it is a property + */ + isProperty(property: $FlowFixMe): boolean; + /** + * Given a property declaration, it returns the key name. + * @parameter property: an object containing a property declaration. + * @parameter hasteModuleName: a string with the native module name. + * @returns: the key name. + * @throws if property does not contain a property declaration. + */ + getKeyName(property: $FlowFixMe, hasteModuleName: string): string; + /** + * @returns: the Parser language. + */ + language(): ParserType; + /** + * Given a type annotation, it returns the type name. + * @parameter typeAnnotation: the annotation for a type in the AST. + * @returns: the name of the type. + */ + getTypeAnnotationName(typeAnnotation: $FlowFixMe): string; + /** + * Given a type arguments, it returns a boolean specifying if the Module is Invalid. + * @parameter typeArguments: the type arguments. + * @returns: a boolean specifying if the Module is Invalid. + */ + checkIfInvalidModule(typeArguments: $FlowFixMe): boolean; + /** + * Given a union annotation members types, it returns an array of remaped members names without duplicates. + * @parameter membersTypes: union annotation members types + * @returns: an array of remaped members names without duplicates. + */ + remapUnionTypeAnnotationMemberNames( + types: $FlowFixMe, + ): UnionTypeAnnotationMemberType[]; + /** + * Given the name of a file, it returns a Schema. + * @parameter filename: the name of the file. + * @returns: the Schema of the file. + */ + parseFile(filename: string): SchemaType; + /** + * Given the content of a file, it returns a Schema. + * @parameter contents: the content of the file. + * @parameter filename: the name of the file. + * @returns: the Schema of the file. + */ + parseString(contents: string, filename: ?string): SchemaType; + /** + * Given the name of a file, it returns a Schema. + * @parameter filename: the name of the file. + * @returns: the Schema of the file. + */ + parseModuleFixture(filename: string): SchemaType; + + /** + * Given the content of a file, it returns an AST. + * @parameter contents: the content of the file. + * @parameter filename: the name of the file, if available. + * @throws if there is a syntax error. + * @returns: the AST of the file. + */ + getAst(contents: string, filename?: ?string): $FlowFixMe; + + /** + * Given a FunctionTypeAnnotation, it returns an array of its parameters. + * @parameter functionTypeAnnotation: a FunctionTypeAnnotation + * @returns: the parameters of the FunctionTypeAnnotation. + */ + getFunctionTypeAnnotationParameters( + functionTypeAnnotation: $FlowFixMe, + ): $ReadOnlyArray<$FlowFixMe>; + + /** + * Given a parameter, it returns the function name of the parameter. + * @parameter parameter: a parameter of a FunctionTypeAnnotation. + * @returns: the function name of the parameter. + */ + getFunctionNameFromParameter( + parameter: NamedShape>, + ): $FlowFixMe; + + /** + * Given a parameter, it returns its name. + * @parameter parameter: a parameter of a FunctionTypeAnnotation. + * @returns: the name of the parameter. + */ + getParameterName(parameter: $FlowFixMe): string; + + /** + * Given a parameter, it returns its typeAnnotation. + * @parameter parameter: a parameter of a FunctionTypeAnnotation. + * @returns: the typeAnnotation of the parameter. + */ + getParameterTypeAnnotation(param: $FlowFixMe): $FlowFixMe; + + /** + * Given a FunctionTypeAnnotation, it returns its returnType. + * @parameter functionTypeAnnotation: a FunctionTypeAnnotation + * @returns: the returnType of the FunctionTypeAnnotation. + */ + getFunctionTypeAnnotationReturnType( + functionTypeAnnotation: $FlowFixMe, + ): $FlowFixMe; + + /** + * Calculates an enum's members type + */ + parseEnumMembersType(typeAnnotation: $FlowFixMe): NativeModuleEnumMemberType; + + /** + * Throws if enum mebers are not supported + */ + validateEnumMembersSupported( + typeAnnotation: $FlowFixMe, + enumMembersType: NativeModuleEnumMemberType, + ): void; + + /** + * Calculates enum's members + */ + parseEnumMembers(typeAnnotation: $FlowFixMe): NativeModuleEnumMembers; + + /** + * Given a node, it returns true if it is a module interface + */ + isModuleInterface(node: $FlowFixMe): boolean; + + /** + * Given a type name, it returns true if it is a generic type annotation + */ + isGenericTypeAnnotation(type: $FlowFixMe): boolean; + + /** + * Given a typeAnnotation, it returns the annotated element. + * @parameter typeAnnotation: the annotation for a type. + * @parameter types: a map of type declarations. + * @returns: the annotated element. + */ + extractAnnotatedElement( + typeAnnotation: $FlowFixMe, + types: TypeDeclarationMap, + ): $FlowFixMe; + + /** + * Given the AST, returns the TypeDeclarationMap + */ + getTypes(ast: $FlowFixMe): TypeDeclarationMap; + + /** + * Given a callExpression, it returns the typeParameters of the callExpression. + * @parameter callExpression: the callExpression. + * @returns: the typeParameters of the callExpression or null if it does not exist. + */ + callExpressionTypeParameters(callExpression: $FlowFixMe): $FlowFixMe | null; + + /** + * Given an array of properties from a Partial type, it returns an array of remaped properties. + * @parameter properties: properties from a Partial types. + * @parameter hasteModuleName: a string with the native module name. + * @parameter types: a map of type declarations. + * @parameter aliasMap: a map of type aliases. + * @parameter enumMap: a map of type enums. + * @parameter tryParse: a parser error capturer. + * @parameter cxxOnly: a boolean specifying if the module is Cxx only. + * @returns: an array of remaped properties + */ + computePartialProperties( + properties: Array<$FlowFixMe>, + hasteModuleName: string, + types: TypeDeclarationMap, + aliasMap: {...NativeModuleAliasMap}, + enumMap: {...NativeModuleEnumMap}, + tryParse: ParserErrorCapturer, + cxxOnly: boolean, + ): Array<$FlowFixMe>; + + /** + * Given a propertyValueType, it returns a boolean specifying if the property is a function type annotation. + * @parameter propertyValueType: the propertyValueType. + * @returns: a boolean specifying if the property is a function type annotation. + */ + functionTypeAnnotation(propertyValueType: string): boolean; + + /** + * Given a declaration, it returns the typeArgumentParams of the declaration. + * @parameter declaration: the declaration. + * @returns: the typeArgumentParams of the declaration. + */ + getTypeArgumentParamsFromDeclaration(declaration: $FlowFixMe): $FlowFixMe; + + /** + * Given a typeArgumentParams and funcArgumentParams it returns a native component type. + * @parameter typeArgumentParams: the typeArgumentParams. + * @parameter funcArgumentParams: the funcArgumentParams. + * @returns: a native component type. + */ + getNativeComponentType( + typeArgumentParams: $FlowFixMe, + funcArgumentParams: $FlowFixMe, + ): {[string]: string}; + + /** + * Given a annotatedElement, it returns the properties of annotated element. + * @parameter annotatedElement: the annotated element. + * @returns: the properties of annotated element. + */ + getAnnotatedElementProperties(annotatedElement: $FlowFixMe): $FlowFixMe; + + /** + * Given a typeAlias, it returns an array of properties. + * @parameter typeAlias: the type alias. + * @returns: an array of properties. + */ + bodyProperties(typeAlias: $FlowFixMe): $ReadOnlyArray<$FlowFixMe>; + + /** + * Given a keyword convert it to TypeAnnotation. + * @parameter keyword + * @returns: converted TypeAnnotation to Keywords + */ + convertKeywordToTypeAnnotation(keyword: string): string; + + /** + * Given a prop return its arguments. + * @parameter prop + * @returns: Arguments of the prop + */ + argumentForProp(prop: PropAST): $FlowFixMe; + + /** + * Given a prop return its name. + * @parameter prop + * @returns: name property + */ + nameForArgument(prop: PropAST): $FlowFixMe; + + /** + * Given a property return if it is optional. + * @parameter property + * @returns: a boolean specifying if the Property is optional + */ + isOptionalProperty(property: $FlowFixMe): boolean; + + getGetTypeAnnotationFN(): GetTypeAnnotationFN; + + getGetSchemaInfoFN(): GetSchemaInfoFN; + + /** + * Given a property return the type annotation. + * @parameter property + * @returns: the annotation for a type in the AST. + */ + getTypeAnnotationFromProperty(property: PropAST): $FlowFixMe; + + getResolvedTypeAnnotation( + typeAnnotation: $FlowFixMe, + types: TypeDeclarationMap, + parser: Parser, + ): { + nullable: boolean, + typeAnnotation: $FlowFixMe, + typeResolutionStatus: TypeResolutionStatus, + }; + + getResolveTypeAnnotationFN(): ResolveTypeAnnotationFN; + + getProps( + typeDefinition: $ReadOnlyArray, + types: TypeDeclarationMap, + ): { + props: $ReadOnlyArray>, + extendsProps: $ReadOnlyArray, + }; + + getProperties(typeName: string, types: TypeDeclarationMap): $FlowFixMe; + + /** + * Given a typeAlias, it returns the next node. + */ + nextNodeForTypeAlias(typeAnnotation: $FlowFixMe): $FlowFixMe; + + /** + * Given an enum Declaration, it returns the next node. + */ + nextNodeForEnum(typeAnnotation: $FlowFixMe): $FlowFixMe; + + /** + * Given a unsupported typeAnnotation, returns an error message. + */ + genericTypeAnnotationErrorMessage(typeAnnotation: $FlowFixMe): string; + + /** + * Given a type annotation, it extracts the type. + * @parameter typeAnnotation: the annotation for a type in the AST. + * @returns: the extracted type. + */ + extractTypeFromTypeAnnotation(typeAnnotation: $FlowFixMe): string; + + /** + * Given a typeAnnotation return the properties of an object. + * @parameter property + * @returns: the properties of an object represented by a type annotation. + */ + getObjectProperties(typeAnnotation: $FlowFixMe): $FlowFixMe; + + /** + * Given a option return the literal value. + * @parameter option + * @returns: the literal value of an union represented. + */ + getLiteralValue(option: $FlowFixMe): $FlowFixMe; + + /** + * Given a type annotation, it returns top level name in the AST if it exists else returns null. + * @parameter typeAnnotation: the annotation for a type in the AST. + * @returns: the top level name properties in the AST if it exists else null. + */ + getPaperTopLevelNameDeprecated(typeAnnotation: $FlowFixMe): $FlowFixMe; +} diff --git a/packages/react-native-codegen/lib/parsers/parserMock.js b/packages/react-native-codegen/lib/parsers/parserMock.js new file mode 100644 index 00000000000000..648bc20eb8aefc --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/parserMock.js @@ -0,0 +1,416 @@ +/** + * 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'; + +function _defineProperty(e, r, t) { + return ( + (r = _toPropertyKey(r)) in e + ? Object.defineProperty(e, r, { + value: t, + enumerable: !0, + configurable: !0, + writable: !0, + }) + : (e[r] = t), + e + ); +} +function _toPropertyKey(t) { + var i = _toPrimitive(t, 'string'); + return 'symbol' == typeof i ? i : i + ''; +} +function _toPrimitive(t, r) { + if ('object' != typeof t || !t) return t; + var e = t[Symbol.toPrimitive]; + if (void 0 !== e) { + var i = e.call(t, r || 'default'); + if ('object' != typeof i) return i; + throw new TypeError('@@toPrimitive must return a primitive value.'); + } + return ('string' === r ? String : Number)(t); +} +import invariant from 'invariant'; +const _require = require('./errors'), + UnsupportedObjectPropertyTypeAnnotationParserError = + _require.UnsupportedObjectPropertyTypeAnnotationParserError; +const _require2 = require('./flow/parseFlowAndThrowErrors'), + parseFlowAndThrowErrors = _require2.parseFlowAndThrowErrors; +const _require3 = require('./parsers-commons'), + buildPropSchema = _require3.buildPropSchema; +const _require4 = require('./typescript/components/componentsUtils'), + flattenProperties = _require4.flattenProperties; +const schemaMock = { + modules: { + StringPropNativeComponentView: { + type: 'Component', + components: { + StringPropNativeComponentView: { + extendsProps: [], + events: [], + props: [], + commands: [], + }, + }, + }, + }, +}; +export class MockedParser { + constructor() { + _defineProperty( + this, + 'typeParameterInstantiation', + 'TypeParameterInstantiation', + ); + _defineProperty(this, 'typeAlias', 'TypeAlias'); + _defineProperty(this, 'enumDeclaration', 'EnumDeclaration'); + _defineProperty(this, 'interfaceDeclaration', 'InterfaceDeclaration'); + _defineProperty( + this, + 'nullLiteralTypeAnnotation', + 'NullLiteralTypeAnnotation', + ); + _defineProperty( + this, + 'undefinedLiteralTypeAnnotation', + 'VoidLiteralTypeAnnotation', + ); + } + isProperty(property) { + return property.type === 'ObjectTypeProperty'; + } + getKeyName(property, hasteModuleName) { + if (!this.isProperty(property)) { + throw new UnsupportedObjectPropertyTypeAnnotationParserError( + hasteModuleName, + property, + property.type, + this.language(), + ); + } + return property.key.name; + } + language() { + return 'Flow'; + } + getTypeAnnotationName(typeAnnotation) { + var _typeAnnotation$id; + return typeAnnotation === null || + typeAnnotation === void 0 || + (_typeAnnotation$id = typeAnnotation.id) === null || + _typeAnnotation$id === void 0 + ? void 0 + : _typeAnnotation$id.name; + } + checkIfInvalidModule(typeArguments) { + return false; + } + remapUnionTypeAnnotationMemberNames(membersTypes) { + return []; + } + parseFile(filename) { + return schemaMock; + } + parseString(contents, filename) { + return schemaMock; + } + parseModuleFixture(filename) { + return schemaMock; + } + getAst(contents, filename) { + return parseFlowAndThrowErrors(contents, { + filename, + }); + } + getFunctionTypeAnnotationParameters(functionTypeAnnotation) { + return functionTypeAnnotation.params; + } + getFunctionNameFromParameter(parameter) { + return parameter.name; + } + getParameterName(parameter) { + return parameter.name.name; + } + getParameterTypeAnnotation(parameter) { + return parameter.typeAnnotation; + } + getFunctionTypeAnnotationReturnType(functionTypeAnnotation) { + return functionTypeAnnotation.returnType; + } + parseEnumMembersType(typeAnnotation) { + return typeAnnotation.type; + } + validateEnumMembersSupported(typeAnnotation, enumMembersType) { + return; + } + parseEnumMembers(typeAnnotation) { + return typeAnnotation.type === 'StringTypeAnnotation' + ? [ + { + name: 'Hello', + value: 'hello', + }, + { + name: 'Goodbye', + value: 'goodbye', + }, + ] + : [ + { + name: 'On', + value: '1', + }, + { + name: 'Off', + value: '0', + }, + ]; + } + isModuleInterface(node) { + return ( + node.type === 'InterfaceDeclaration' && + node.extends.length === 1 && + node.extends[0].type === 'InterfaceExtends' && + node.extends[0].id.name === 'TurboModule' + ); + } + isGenericTypeAnnotation(type) { + return true; + } + extractAnnotatedElement(typeAnnotation, types) { + return types[typeAnnotation.typeParameters.params[0].id.name]; + } + getTypes(ast) { + return {}; + } + callExpressionTypeParameters(callExpression) { + return callExpression.typeArguments || null; + } + computePartialProperties( + properties, + hasteModuleName, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + ) { + return [ + { + name: 'a', + optional: true, + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }, + { + name: 'b', + optional: true, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }, + ]; + } + functionTypeAnnotation(propertyValueType) { + return propertyValueType === 'FunctionTypeAnnotation'; + } + getTypeArgumentParamsFromDeclaration(declaration) { + return declaration.typeArguments.params; + } + getNativeComponentType(typeArgumentParams, funcArgumentParams) { + return { + propsTypeName: typeArgumentParams[0].id.name, + componentName: funcArgumentParams[0].value, + }; + } + getAnnotatedElementProperties(annotatedElement) { + return annotatedElement.right.properties; + } + bodyProperties(typeAlias) { + return typeAlias.body.properties; + } + convertKeywordToTypeAnnotation(keyword) { + return keyword; + } + argumentForProp(prop) { + return prop.expression; + } + nameForArgument(prop) { + return prop.expression.name; + } + isOptionalProperty(property) { + return property.optional || false; + } + getTypeAnnotationFromProperty(property) { + return property.typeAnnotation.typeAnnotation; + } + getGetTypeAnnotationFN() { + return () => { + return {}; + }; + } + getGetSchemaInfoFN() { + return () => { + return { + name: 'MockedSchema', + optional: false, + typeAnnotation: 'BooleanTypeAnnotation', + defaultValue: false, + withNullDefault: false, + }; + }; + } + getResolveTypeAnnotationFN() { + return () => { + return { + nullable: false, + typeAnnotation: null, + typeResolutionStatus: { + successful: false, + }, + }; + }; + } + getResolvedTypeAnnotation(typeAnnotation, types) { + invariant( + typeAnnotation != null, + 'resolveTypeAnnotation(): typeAnnotation cannot be null', + ); + let node = typeAnnotation; + let nullable = false; + let typeResolutionStatus = { + successful: false, + }; + for (;;) { + if (node.type === 'NullableTypeAnnotation') { + nullable = true; + node = node.typeAnnotation; + continue; + } + if (node.type !== 'GenericTypeAnnotation') { + break; + } + const resolvedTypeAnnotation = types[node.id.name]; + if (resolvedTypeAnnotation == null) { + break; + } + switch (resolvedTypeAnnotation.type) { + case 'TypeAlias': { + typeResolutionStatus = { + successful: true, + type: 'alias', + name: node.id.name, + }; + node = resolvedTypeAnnotation.right; + break; + } + case 'EnumDeclaration': { + typeResolutionStatus = { + successful: true, + type: 'enum', + name: node.id.name, + }; + node = resolvedTypeAnnotation.body; + break; + } + default: { + throw new TypeError( + `A non GenericTypeAnnotation must be a type declaration ('TypeAlias') or enum ('EnumDeclaration'). Instead, got the unsupported ${resolvedTypeAnnotation.type}.`, + ); + } + } + } + return { + nullable: nullable, + typeAnnotation: node, + typeResolutionStatus, + }; + } + getExtendsProps(typeDefinition, types) { + return typeDefinition + .filter(prop => prop.type === 'ObjectTypeSpreadProperty') + .map(prop => this.extendsForProp(prop, types, this)) + .filter(Boolean); + } + extendsForProp(prop, types, parser) { + const argument = this.argumentForProp(prop); + if (!argument) { + console.log('null', prop); + } + const name = parser.nameForArgument(prop); + 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}`); + } + } + } + removeKnownExtends(typeDefinition, types) { + return typeDefinition.filter( + prop => + prop.type !== 'ObjectTypeSpreadProperty' || + this.extendsForProp(prop, types, this) === null, + ); + } + getProps(typeDefinition, types) { + const nonExtendsProps = this.removeKnownExtends(typeDefinition, types); + const props = flattenProperties(nonExtendsProps, types, this) + .map(property => buildPropSchema(property, types, this)) + .filter(Boolean); + return { + props, + extendsProps: this.getExtendsProps(typeDefinition, types), + }; + } + getProperties(typeName, types) { + const typeAlias = types[typeName]; + try { + return typeAlias.right.typeParameters.params[0].properties; + } catch (e) { + throw new Error( + `Failed to find type definition for "${typeName}", please check that you have a valid codegen flow file`, + ); + } + } + nextNodeForTypeAlias(typeAnnotation) { + return typeAnnotation.right; + } + nextNodeForEnum(typeAnnotation) { + return typeAnnotation.body; + } + genericTypeAnnotationErrorMessage(typeAnnotation) { + return `A non GenericTypeAnnotation must be a type declaration ('${this.typeAlias}') or enum ('${this.enumDeclaration}'). Instead, got the unsupported ${typeAnnotation.type}.`; + } + extractTypeFromTypeAnnotation(typeAnnotation) { + return typeAnnotation.type === 'GenericTypeAnnotation' + ? typeAnnotation.id.name + : typeAnnotation.type; + } + getObjectProperties(typeAnnotation) { + return typeAnnotation.properties; + } + getLiteralValue(option) { + return option.value; + } + getPaperTopLevelNameDeprecated(typeAnnotation) { + return typeAnnotation.typeParameters.params.length > 1 + ? typeAnnotation.typeParameters.params[1].value + : null; + } +} diff --git a/packages/react-native-codegen/lib/parsers/parserMock.js.flow b/packages/react-native-codegen/lib/parsers/parserMock.js.flow new file mode 100644 index 00000000000000..7b0cc7ed53f093 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/parserMock.js.flow @@ -0,0 +1,492 @@ +/** + * 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 { + ExtendsPropsShape, + NamedShape, + NativeModuleAliasMap, + NativeModuleEnumMap, + NativeModuleEnumMembers, + NativeModuleEnumMemberType, + NativeModuleParamTypeAnnotation, + Nullable, + PropTypeAnnotation, + SchemaType, + UnionTypeAnnotationMemberType, +} from '../CodegenSchema'; +import type {ParserType} from './errors'; +import type { + GetSchemaInfoFN, + GetTypeAnnotationFN, + Parser, + ResolveTypeAnnotationFN, +} from './parser'; +import type { + ParserErrorCapturer, + PropAST, + TypeDeclarationMap, + TypeResolutionStatus, +} from './utils'; + +import invariant from 'invariant'; + +const { + UnsupportedObjectPropertyTypeAnnotationParserError, +} = require('./errors'); +const {parseFlowAndThrowErrors} = require('./flow/parseFlowAndThrowErrors'); +const {buildPropSchema} = require('./parsers-commons'); +const {flattenProperties} = require('./typescript/components/componentsUtils'); + +type ExtendsForProp = null | { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', +}; + +const schemaMock = { + modules: { + StringPropNativeComponentView: { + type: 'Component', + components: { + StringPropNativeComponentView: { + extendsProps: [], + events: [], + props: [], + commands: [], + }, + }, + }, + }, +}; + +export class MockedParser implements Parser { + typeParameterInstantiation: string = 'TypeParameterInstantiation'; + typeAlias: string = 'TypeAlias'; + enumDeclaration: string = 'EnumDeclaration'; + interfaceDeclaration: string = 'InterfaceDeclaration'; + nullLiteralTypeAnnotation: string = 'NullLiteralTypeAnnotation'; + undefinedLiteralTypeAnnotation: string = 'VoidLiteralTypeAnnotation'; + + isProperty(property: $FlowFixMe): boolean { + return property.type === 'ObjectTypeProperty'; + } + + getKeyName(property: $FlowFixMe, hasteModuleName: string): string { + if (!this.isProperty(property)) { + throw new UnsupportedObjectPropertyTypeAnnotationParserError( + hasteModuleName, + property, + property.type, + this.language(), + ); + } + return property.key.name; + } + + language(): ParserType { + return 'Flow'; + } + + getTypeAnnotationName(typeAnnotation: $FlowFixMe): string { + return typeAnnotation?.id?.name; + } + + checkIfInvalidModule(typeArguments: $FlowFixMe): boolean { + return false; + } + + remapUnionTypeAnnotationMemberNames( + membersTypes: $FlowFixMe[], + ): UnionTypeAnnotationMemberType[] { + return []; + } + + parseFile(filename: string): SchemaType { + return schemaMock; + } + + parseString(contents: string, filename: ?string): SchemaType { + return schemaMock; + } + + parseModuleFixture(filename: string): SchemaType { + return schemaMock; + } + + getAst(contents: string, filename?: ?string): $FlowFixMe { + return parseFlowAndThrowErrors(contents, {filename}); + } + + getFunctionTypeAnnotationParameters( + functionTypeAnnotation: $FlowFixMe, + ): $ReadOnlyArray<$FlowFixMe> { + return functionTypeAnnotation.params; + } + + getFunctionNameFromParameter( + parameter: NamedShape>, + ): $FlowFixMe { + return parameter.name; + } + + getParameterName(parameter: $FlowFixMe): string { + return parameter.name.name; + } + + getParameterTypeAnnotation(parameter: $FlowFixMe): $FlowFixMe { + return parameter.typeAnnotation; + } + + getFunctionTypeAnnotationReturnType( + functionTypeAnnotation: $FlowFixMe, + ): $FlowFixMe { + return functionTypeAnnotation.returnType; + } + + parseEnumMembersType(typeAnnotation: $FlowFixMe): NativeModuleEnumMemberType { + return typeAnnotation.type; + } + + validateEnumMembersSupported( + typeAnnotation: $FlowFixMe, + enumMembersType: NativeModuleEnumMemberType, + ): void { + return; + } + + parseEnumMembers(typeAnnotation: $FlowFixMe): NativeModuleEnumMembers { + return typeAnnotation.type === 'StringTypeAnnotation' + ? [ + { + name: 'Hello', + value: 'hello', + }, + { + name: 'Goodbye', + value: 'goodbye', + }, + ] + : [ + { + name: 'On', + value: '1', + }, + { + name: 'Off', + value: '0', + }, + ]; + } + + isModuleInterface(node: $FlowFixMe): boolean { + return ( + node.type === 'InterfaceDeclaration' && + node.extends.length === 1 && + node.extends[0].type === 'InterfaceExtends' && + node.extends[0].id.name === 'TurboModule' + ); + } + + isGenericTypeAnnotation(type: $FlowFixMe): boolean { + return true; + } + + extractAnnotatedElement( + typeAnnotation: $FlowFixMe, + types: TypeDeclarationMap, + ): $FlowFixMe { + return types[typeAnnotation.typeParameters.params[0].id.name]; + } + + getTypes(ast: $FlowFixMe): TypeDeclarationMap { + return {}; + } + + callExpressionTypeParameters(callExpression: $FlowFixMe): $FlowFixMe | null { + return callExpression.typeArguments || null; + } + + computePartialProperties( + properties: Array<$FlowFixMe>, + hasteModuleName: string, + types: TypeDeclarationMap, + aliasMap: {...NativeModuleAliasMap}, + enumMap: {...NativeModuleEnumMap}, + tryParse: ParserErrorCapturer, + cxxOnly: boolean, + ): Array<$FlowFixMe> { + return [ + { + name: 'a', + optional: true, + typeAnnotation: {type: 'StringTypeAnnotation'}, + }, + { + name: 'b', + optional: true, + typeAnnotation: {type: 'BooleanTypeAnnotation'}, + }, + ]; + } + + functionTypeAnnotation(propertyValueType: string): boolean { + return propertyValueType === 'FunctionTypeAnnotation'; + } + + getTypeArgumentParamsFromDeclaration(declaration: $FlowFixMe): $FlowFixMe { + return declaration.typeArguments.params; + } + + getNativeComponentType( + typeArgumentParams: $FlowFixMe, + funcArgumentParams: $FlowFixMe, + ): {[string]: string} { + return { + propsTypeName: typeArgumentParams[0].id.name, + componentName: funcArgumentParams[0].value, + }; + } + + getAnnotatedElementProperties(annotatedElement: $FlowFixMe): $FlowFixMe { + return annotatedElement.right.properties; + } + + bodyProperties(typeAlias: $FlowFixMe): $ReadOnlyArray<$FlowFixMe> { + return typeAlias.body.properties; + } + + convertKeywordToTypeAnnotation(keyword: string): string { + return keyword; + } + + argumentForProp(prop: PropAST): $FlowFixMe { + return prop.expression; + } + + nameForArgument(prop: PropAST): $FlowFixMe { + return prop.expression.name; + } + + isOptionalProperty(property: $FlowFixMe): boolean { + return property.optional || false; + } + + getTypeAnnotationFromProperty(property: PropAST): $FlowFixMe { + return property.typeAnnotation.typeAnnotation; + } + + getGetTypeAnnotationFN(): GetTypeAnnotationFN { + return () => { + return {}; + }; + } + + getGetSchemaInfoFN(): GetSchemaInfoFN { + return () => { + return { + name: 'MockedSchema', + optional: false, + typeAnnotation: 'BooleanTypeAnnotation', + defaultValue: false, + withNullDefault: false, + }; + }; + } + + getResolveTypeAnnotationFN(): ResolveTypeAnnotationFN { + return () => { + return { + nullable: false, + typeAnnotation: null, + typeResolutionStatus: {successful: false}, + }; + }; + } + + getResolvedTypeAnnotation( + typeAnnotation: $FlowFixMe, + types: TypeDeclarationMap, + ): { + nullable: boolean, + typeAnnotation: $FlowFixMe, + typeResolutionStatus: TypeResolutionStatus, + } { + invariant( + typeAnnotation != null, + 'resolveTypeAnnotation(): typeAnnotation cannot be null', + ); + + let node = typeAnnotation; + let nullable = false; + let typeResolutionStatus: TypeResolutionStatus = { + successful: false, + }; + + for (;;) { + if (node.type === 'NullableTypeAnnotation') { + nullable = true; + node = node.typeAnnotation; + continue; + } + + if (node.type !== 'GenericTypeAnnotation') { + break; + } + + const resolvedTypeAnnotation = types[node.id.name]; + if (resolvedTypeAnnotation == null) { + break; + } + + switch (resolvedTypeAnnotation.type) { + case 'TypeAlias': { + typeResolutionStatus = { + successful: true, + type: 'alias', + name: node.id.name, + }; + node = resolvedTypeAnnotation.right; + break; + } + case 'EnumDeclaration': { + typeResolutionStatus = { + successful: true, + type: 'enum', + name: node.id.name, + }; + node = resolvedTypeAnnotation.body; + break; + } + default: { + throw new TypeError( + `A non GenericTypeAnnotation must be a type declaration ('TypeAlias') or enum ('EnumDeclaration'). Instead, got the unsupported ${resolvedTypeAnnotation.type}.`, + ); + } + } + } + + return { + nullable: nullable, + typeAnnotation: node, + typeResolutionStatus, + }; + } + + getExtendsProps( + typeDefinition: $ReadOnlyArray, + types: TypeDeclarationMap, + ): $ReadOnlyArray { + return typeDefinition + .filter(prop => prop.type === 'ObjectTypeSpreadProperty') + .map(prop => this.extendsForProp(prop, types, this)) + .filter(Boolean); + } + + extendsForProp( + prop: PropAST, + types: TypeDeclarationMap, + parser: Parser, + ): ExtendsForProp { + const argument = this.argumentForProp(prop); + if (!argument) { + console.log('null', prop); + } + const name = parser.nameForArgument(prop); + + 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}`); + } + } + } + + removeKnownExtends( + typeDefinition: $ReadOnlyArray, + types: TypeDeclarationMap, + ): $ReadOnlyArray { + return typeDefinition.filter( + prop => + prop.type !== 'ObjectTypeSpreadProperty' || + this.extendsForProp(prop, types, this) === null, + ); + } + + getProps( + typeDefinition: $ReadOnlyArray, + types: TypeDeclarationMap, + ): { + props: $ReadOnlyArray>, + extendsProps: $ReadOnlyArray, + } { + const nonExtendsProps = this.removeKnownExtends(typeDefinition, types); + const props = flattenProperties(nonExtendsProps, types, this) + .map(property => buildPropSchema(property, types, this)) + .filter(Boolean); + + return { + props, + extendsProps: this.getExtendsProps(typeDefinition, types), + }; + } + + getProperties(typeName: string, types: TypeDeclarationMap): $FlowFixMe { + const typeAlias = types[typeName]; + try { + return typeAlias.right.typeParameters.params[0].properties; + } catch (e) { + throw new Error( + `Failed to find type definition for "${typeName}", please check that you have a valid codegen flow file`, + ); + } + } + + nextNodeForTypeAlias(typeAnnotation: $FlowFixMe): $FlowFixMe { + return typeAnnotation.right; + } + + nextNodeForEnum(typeAnnotation: $FlowFixMe): $FlowFixMe { + return typeAnnotation.body; + } + + genericTypeAnnotationErrorMessage(typeAnnotation: $FlowFixMe): string { + return `A non GenericTypeAnnotation must be a type declaration ('${this.typeAlias}') or enum ('${this.enumDeclaration}'). Instead, got the unsupported ${typeAnnotation.type}.`; + } + + extractTypeFromTypeAnnotation(typeAnnotation: $FlowFixMe): string { + return typeAnnotation.type === 'GenericTypeAnnotation' + ? typeAnnotation.id.name + : typeAnnotation.type; + } + + getObjectProperties(typeAnnotation: $FlowFixMe): $FlowFixMe { + return typeAnnotation.properties; + } + + getLiteralValue(option: $FlowFixMe): $FlowFixMe { + return option.value; + } + + getPaperTopLevelNameDeprecated(typeAnnotation: $FlowFixMe): $FlowFixMe { + return typeAnnotation.typeParameters.params.length > 1 + ? typeAnnotation.typeParameters.params[1].value + : null; + } +} diff --git a/packages/react-native-codegen/lib/parsers/parsers-commons.js b/packages/react-native-codegen/lib/parsers/parsers-commons.js new file mode 100644 index 00000000000000..06e5c0f6611fff --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/parsers-commons.js @@ -0,0 +1,1319 @@ +/** + * 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'; + +function ownKeys(e, r) { + var t = Object.keys(e); + if (Object.getOwnPropertySymbols) { + var o = Object.getOwnPropertySymbols(e); + r && + (o = o.filter(function (r) { + return Object.getOwnPropertyDescriptor(e, r).enumerable; + })), + t.push.apply(t, o); + } + return t; +} +function _objectSpread(e) { + for (var r = 1; r < arguments.length; r++) { + var t = null != arguments[r] ? arguments[r] : {}; + r % 2 + ? ownKeys(Object(t), !0).forEach(function (r) { + _defineProperty(e, r, t[r]); + }) + : Object.getOwnPropertyDescriptors + ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) + : ownKeys(Object(t)).forEach(function (r) { + Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); + }); + } + return e; +} +function _defineProperty(e, r, t) { + return ( + (r = _toPropertyKey(r)) in e + ? Object.defineProperty(e, r, { + value: t, + enumerable: !0, + configurable: !0, + writable: !0, + }) + : (e[r] = t), + e + ); +} +function _toPropertyKey(t) { + var i = _toPrimitive(t, 'string'); + return 'symbol' == typeof i ? i : i + ''; +} +function _toPrimitive(t, r) { + if ('object' != typeof t || !t) return t; + var e = t[Symbol.toPrimitive]; + if (void 0 !== e) { + var i = e.call(t, r || 'default'); + if ('object' != typeof i) return i; + throw new TypeError('@@toPrimitive must return a primitive value.'); + } + return ('string' === r ? String : Number)(t); +} +function _slicedToArray(r, e) { + return ( + _arrayWithHoles(r) || + _iterableToArrayLimit(r, e) || + _unsupportedIterableToArray(r, e) || + _nonIterableRest() + ); +} +function _nonIterableRest() { + throw new TypeError( + 'Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.', + ); +} +function _unsupportedIterableToArray(r, a) { + if (r) { + if ('string' == typeof r) return _arrayLikeToArray(r, a); + var t = {}.toString.call(r).slice(8, -1); + return ( + 'Object' === t && r.constructor && (t = r.constructor.name), + 'Map' === t || 'Set' === t + ? Array.from(r) + : 'Arguments' === t || + /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) + ? _arrayLikeToArray(r, a) + : void 0 + ); + } +} +function _arrayLikeToArray(r, a) { + (null == a || a > r.length) && (a = r.length); + for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; + return n; +} +function _iterableToArrayLimit(r, l) { + var t = + null == r + ? null + : ('undefined' != typeof Symbol && r[Symbol.iterator]) || r['@@iterator']; + if (null != t) { + var e, + n, + i, + u, + a = [], + f = !0, + o = !1; + try { + if (((i = (t = t.call(r)).next), 0 === l)) { + if (Object(t) !== t) return; + f = !1; + } else + for ( + ; + !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); + f = !0 + ); + } catch (r) { + (o = !0), (n = r); + } finally { + try { + if (!f && null != t.return && ((u = t.return()), Object(u) !== u)) + return; + } finally { + if (o) throw n; + } + } + return a; + } +} +function _arrayWithHoles(r) { + if (Array.isArray(r)) return r; +} +const _require = require('./error-utils'), + throwIfConfigNotfound = _require.throwIfConfigNotfound, + throwIfEventEmitterEventTypeIsUnsupported = + _require.throwIfEventEmitterEventTypeIsUnsupported, + throwIfEventEmitterTypeIsUnsupported = + _require.throwIfEventEmitterTypeIsUnsupported, + throwIfIncorrectModuleRegistryCallArgument = + _require.throwIfIncorrectModuleRegistryCallArgument, + throwIfIncorrectModuleRegistryCallTypeParameterParserError = + _require.throwIfIncorrectModuleRegistryCallTypeParameterParserError, + throwIfModuleInterfaceIsMisnamed = _require.throwIfModuleInterfaceIsMisnamed, + throwIfModuleInterfaceNotFound = _require.throwIfModuleInterfaceNotFound, + throwIfModuleTypeIsUnsupported = _require.throwIfModuleTypeIsUnsupported, + throwIfMoreThanOneCodegenNativecommands = + _require.throwIfMoreThanOneCodegenNativecommands, + throwIfMoreThanOneConfig = _require.throwIfMoreThanOneConfig, + throwIfMoreThanOneModuleInterfaceParserError = + _require.throwIfMoreThanOneModuleInterfaceParserError, + throwIfMoreThanOneModuleRegistryCalls = + _require.throwIfMoreThanOneModuleRegistryCalls, + throwIfPropertyValueTypeIsUnsupported = + _require.throwIfPropertyValueTypeIsUnsupported, + throwIfTypeAliasIsNotInterface = _require.throwIfTypeAliasIsNotInterface, + throwIfUnsupportedFunctionParamTypeAnnotationParserError = + _require.throwIfUnsupportedFunctionParamTypeAnnotationParserError, + throwIfUnsupportedFunctionReturnTypeAnnotationParserError = + _require.throwIfUnsupportedFunctionReturnTypeAnnotationParserError, + throwIfUntypedModule = _require.throwIfUntypedModule, + throwIfUnusedModuleInterfaceParserError = + _require.throwIfUnusedModuleInterfaceParserError, + throwIfWrongNumberOfCallExpressionArgs = + _require.throwIfWrongNumberOfCallExpressionArgs; +const _require2 = require('./errors'), + MissingTypeParameterGenericParserError = + _require2.MissingTypeParameterGenericParserError, + MoreThanOneTypeParameterGenericParserError = + _require2.MoreThanOneTypeParameterGenericParserError, + UnnamedFunctionParamParserError = _require2.UnnamedFunctionParamParserError, + UnsupportedObjectDirectRecursivePropertyParserError = + _require2.UnsupportedObjectDirectRecursivePropertyParserError; +const _require3 = require('./utils'), + createParserErrorCapturer = _require3.createParserErrorCapturer, + extractNativeModuleName = _require3.extractNativeModuleName, + getConfigType = _require3.getConfigType, + getSortedObject = _require3.getSortedObject, + isModuleRegistryCall = _require3.isModuleRegistryCall, + verifyPlatforms = _require3.verifyPlatforms, + visit = _require3.visit; +const invariant = require('invariant'); + +// $FlowFixMe[unclear-type] TODO(T108222691): Use flow-types for @babel/parser + +function wrapModuleSchema(nativeModuleSchema, hasteModuleName) { + return { + modules: { + [hasteModuleName]: nativeModuleSchema, + }, + }; +} + +// $FlowFixMe[unsupported-variance-annotation] +function unwrapNullable(x) { + if (x.type === 'NullableTypeAnnotation') { + return [x.typeAnnotation, true]; + } + return [x, false]; +} + +// $FlowFixMe[unsupported-variance-annotation] +function wrapNullable(nullable, typeAnnotation) { + if (!nullable) { + return typeAnnotation; + } + return { + type: 'NullableTypeAnnotation', + typeAnnotation, + }; +} +function assertGenericTypeAnnotationHasExactlyOneTypeParameter( + moduleName, + /** + * TODO(T108222691): Use flow-types for @babel/parser + */ + typeAnnotation, + parser, +) { + if (typeAnnotation.typeParameters == null) { + throw new MissingTypeParameterGenericParserError( + moduleName, + typeAnnotation, + parser, + ); + } + const typeAnnotationType = parser.typeParameterInstantiation; + invariant( + typeAnnotation.typeParameters.type === typeAnnotationType, + `assertGenericTypeAnnotationHasExactlyOneTypeParameter: Type parameters must be an AST node of type '${typeAnnotationType}'`, + ); + if (typeAnnotation.typeParameters.params.length !== 1) { + throw new MoreThanOneTypeParameterGenericParserError( + moduleName, + typeAnnotation, + parser, + ); + } +} +function isObjectProperty(property, language) { + switch (language) { + case 'Flow': + return property.type === 'ObjectTypeProperty'; + case 'TypeScript': + return property.type === 'TSPropertySignature'; + default: + return false; + } +} +function getObjectTypeAnnotations( + hasteModuleName, + types, + tryParse, + translateTypeAnnotation, + parser, +) { + const aliasMap = {}; + Object.entries(types).forEach(([key, value]) => { + const isTypeAlias = + value.type === 'TypeAlias' || value.type === 'TSTypeAliasDeclaration'; + if (!isTypeAlias) { + return; + } + const parent = parser.nextNodeForTypeAlias(value); + if ( + parent.type !== 'ObjectTypeAnnotation' && + parent.type !== 'TSTypeLiteral' + ) { + return; + } + const typeProperties = parser + .getAnnotatedElementProperties(value) + .map(prop => + parseObjectProperty( + parent, + prop, + hasteModuleName, + types, + aliasMap, + {}, + // enumMap + tryParse, + true, + // cxxOnly + (prop === null || prop === void 0 ? void 0 : prop.optional) || false, + translateTypeAnnotation, + parser, + ), + ); + aliasMap[key] = { + type: 'ObjectTypeAnnotation', + properties: typeProperties, + }; + }); + return aliasMap; +} +function parseObjectProperty( + parentObject, + property, + hasteModuleName, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + nullable, + translateTypeAnnotation, + parser, +) { + const language = parser.language(); + const name = parser.getKeyName(property, hasteModuleName); + const _property$optional = property.optional, + optional = _property$optional === void 0 ? false : _property$optional; + const languageTypeAnnotation = + language === 'TypeScript' + ? property.typeAnnotation.typeAnnotation + : property.value; + + // Handle recursive types + if (parentObject) { + var _languageTypeAnnotati, _languageTypeAnnotati2; + const propertyType = parser.getResolveTypeAnnotationFN()( + languageTypeAnnotation, + types, + parser, + ); + if ( + propertyType.typeResolutionStatus.successful === true && + propertyType.typeResolutionStatus.type === 'alias' && + (language === 'TypeScript' + ? parentObject.typeName && + parentObject.typeName.name === + ((_languageTypeAnnotati = languageTypeAnnotation.typeName) === + null || _languageTypeAnnotati === void 0 + ? void 0 + : _languageTypeAnnotati.name) + : parentObject.id && + parentObject.id.name === + ((_languageTypeAnnotati2 = languageTypeAnnotation.id) === null || + _languageTypeAnnotati2 === void 0 + ? void 0 + : _languageTypeAnnotati2.name)) + ) { + if (!optional) { + throw new UnsupportedObjectDirectRecursivePropertyParserError( + name, + languageTypeAnnotation, + hasteModuleName, + ); + } + return { + name, + optional, + typeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: propertyType.typeResolutionStatus.name, + }, + }; + } + } + + // Handle non-recursive types + const _unwrapNullable = unwrapNullable( + translateTypeAnnotation( + hasteModuleName, + languageTypeAnnotation, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + parser, + ), + ), + _unwrapNullable2 = _slicedToArray(_unwrapNullable, 2), + propertyTypeAnnotation = _unwrapNullable2[0], + isPropertyNullable = _unwrapNullable2[1]; + if ( + (propertyTypeAnnotation.type === 'FunctionTypeAnnotation' && !cxxOnly) || + propertyTypeAnnotation.type === 'PromiseTypeAnnotation' || + propertyTypeAnnotation.type === 'VoidTypeAnnotation' + ) { + throwIfPropertyValueTypeIsUnsupported( + hasteModuleName, + languageTypeAnnotation, + property.key, + propertyTypeAnnotation.type, + ); + } + return { + name, + optional, + typeAnnotation: wrapNullable(isPropertyNullable, propertyTypeAnnotation), + }; +} +function translateFunctionTypeAnnotation( + hasteModuleName, + // TODO(T108222691): Use flow-types for @babel/parser + // TODO(T71778680): This is a FunctionTypeAnnotation. Type this. + functionTypeAnnotation, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + translateTypeAnnotation, + parser, +) { + const params = []; + for (const param of parser.getFunctionTypeAnnotationParameters( + functionTypeAnnotation, + )) { + const parsedParam = tryParse(() => { + if (parser.getFunctionNameFromParameter(param) == null) { + throw new UnnamedFunctionParamParserError(param, hasteModuleName); + } + const paramName = parser.getParameterName(param); + const _unwrapNullable3 = unwrapNullable( + translateTypeAnnotation( + hasteModuleName, + parser.getParameterTypeAnnotation(param), + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + parser, + ), + ), + _unwrapNullable4 = _slicedToArray(_unwrapNullable3, 2), + paramTypeAnnotation = _unwrapNullable4[0], + isParamTypeAnnotationNullable = _unwrapNullable4[1]; + if ( + paramTypeAnnotation.type === 'VoidTypeAnnotation' || + paramTypeAnnotation.type === 'PromiseTypeAnnotation' + ) { + return throwIfUnsupportedFunctionParamTypeAnnotationParserError( + hasteModuleName, + param.typeAnnotation, + paramName, + paramTypeAnnotation.type, + ); + } + return { + name: paramName, + optional: Boolean(param.optional), + typeAnnotation: wrapNullable( + isParamTypeAnnotationNullable, + paramTypeAnnotation, + ), + }; + }); + if (parsedParam != null) { + params.push(parsedParam); + } + } + const _unwrapNullable5 = unwrapNullable( + translateTypeAnnotation( + hasteModuleName, + parser.getFunctionTypeAnnotationReturnType(functionTypeAnnotation), + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + parser, + ), + ), + _unwrapNullable6 = _slicedToArray(_unwrapNullable5, 2), + returnTypeAnnotation = _unwrapNullable6[0], + isReturnTypeAnnotationNullable = _unwrapNullable6[1]; + throwIfUnsupportedFunctionReturnTypeAnnotationParserError( + hasteModuleName, + functionTypeAnnotation, + 'FunctionTypeAnnotation', + cxxOnly, + returnTypeAnnotation.type, + ); + return { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: wrapNullable( + isReturnTypeAnnotationNullable, + returnTypeAnnotation, + ), + params, + }; +} +function buildPropertySchema( + hasteModuleName, + // TODO(T108222691): [TS] Use flow-types for @babel/parser + // TODO(T71778680): [Flow] This is an ObjectTypeProperty containing either: + // - a FunctionTypeAnnotation or GenericTypeAnnotation + // - a NullableTypeAnnoation containing a FunctionTypeAnnotation or GenericTypeAnnotation + // Flow type this node + property, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + translateTypeAnnotation, + parser, +) { + let nullable = false; + let key = property.key, + value = property.value; + const methodName = key.name; + if (parser.language() === 'TypeScript') { + value = + property.type === 'TSMethodSignature' + ? property + : property.typeAnnotation; + } + const resolveTypeAnnotationFN = parser.getResolveTypeAnnotationFN(); + var _resolveTypeAnnotatio = resolveTypeAnnotationFN(value, types, parser); + nullable = _resolveTypeAnnotatio.nullable; + value = _resolveTypeAnnotatio.typeAnnotation; + throwIfModuleTypeIsUnsupported( + hasteModuleName, + property.value, + key.name, + value.type, + parser, + ); + return { + name: methodName, + optional: Boolean(property.optional), + typeAnnotation: wrapNullable( + nullable, + translateFunctionTypeAnnotation( + hasteModuleName, + value, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + translateTypeAnnotation, + parser, + ), + ), + }; +} +function buildEventEmitterSchema( + hasteModuleName, + // TODO(T108222691): [TS] Use flow-types for @babel/parser + // TODO(T71778680): [Flow] This is an ObjectTypeProperty containing either: + // - a FunctionTypeAnnotation or GenericTypeAnnotation + // - a NullableTypeAnnoation containing a FunctionTypeAnnotation or GenericTypeAnnotation + // Flow type this node + property, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + translateTypeAnnotation, + parser, +) { + let key = property.key, + value = property.value; + const eventemitterName = key.name; + const resolveTypeAnnotationFN = parser.getResolveTypeAnnotationFN(); + const _unwrapNullable7 = unwrapNullable(value), + _unwrapNullable8 = _slicedToArray(_unwrapNullable7, 2), + typeAnnotation = _unwrapNullable8[0], + typeAnnotationNullable = _unwrapNullable8[1]; + const typeAnnotationUntyped = + value.typeParameters.params.length === 1 && + value.typeParameters.params[0].type === 'ObjectTypeAnnotation' && + value.typeParameters.params[0].properties.length === 0; + throwIfEventEmitterTypeIsUnsupported( + hasteModuleName, + key.name, + typeAnnotation.type, + parser, + typeAnnotationNullable, + typeAnnotationUntyped, + cxxOnly, + ); + const eventTypeResolutionStatus = resolveTypeAnnotationFN( + typeAnnotation.typeParameters.params[0], + types, + parser, + ); + throwIfEventEmitterEventTypeIsUnsupported( + hasteModuleName, + key.name, + eventTypeResolutionStatus.typeAnnotation, + parser, + eventTypeResolutionStatus.nullable, + ); + return { + name: eventemitterName, + optional: false, + typeAnnotation: { + type: 'EventEmitterTypeAnnotation', + typeAnnotation: { + type: eventTypeResolutionStatus.typeAnnotation.type, + }, + }, + }; +} +function buildSchemaFromConfigType( + configType, + filename, + ast, + wrapComponentSchema, + buildComponentSchema, + buildModuleSchema, + parser, + translateTypeAnnotation, +) { + switch (configType) { + case 'component': { + return wrapComponentSchema(buildComponentSchema(ast, parser)); + } + case 'module': { + if (filename === undefined || filename === null) { + throw new Error('Filepath expected while parasing a module'); + } + const nativeModuleName = extractNativeModuleName(filename); + const _createParserErrorCap = createParserErrorCapturer(), + _createParserErrorCap2 = _slicedToArray(_createParserErrorCap, 2), + parsingErrors = _createParserErrorCap2[0], + tryParse = _createParserErrorCap2[1]; + const schema = tryParse(() => + buildModuleSchema( + nativeModuleName, + ast, + tryParse, + parser, + translateTypeAnnotation, + ), + ); + if (parsingErrors.length > 0) { + /** + * TODO(T77968131): We have two options: + * - Throw the first error, but indicate there are more then one errors. + * - Display all errors, nicely formatted. + * + * For the time being, we're just throw the first error. + **/ + + throw parsingErrors[0]; + } + invariant( + schema != null, + 'When there are no parsing errors, the schema should not be null', + ); + return wrapModuleSchema(schema, nativeModuleName); + } + default: + return { + modules: {}, + }; + } +} +function buildSchema( + contents, + filename, + wrapComponentSchema, + buildComponentSchema, + buildModuleSchema, + Visitor, + parser, + translateTypeAnnotation, +) { + // Early return for non-Spec JavaScript files + if ( + !contents.includes('codegenNativeComponent') && + !contents.includes('TurboModule') + ) { + return { + modules: {}, + }; + } + const ast = parser.getAst(contents, filename); + const configType = getConfigType(ast, Visitor); + return buildSchemaFromConfigType( + configType, + filename, + ast, + wrapComponentSchema, + buildComponentSchema, + buildModuleSchema, + parser, + translateTypeAnnotation, + ); +} +function createComponentConfig(foundConfig, commandsTypeNames) { + return _objectSpread( + _objectSpread({}, foundConfig), + {}, + { + commandTypeName: + commandsTypeNames[0] == null + ? null + : commandsTypeNames[0].commandTypeName, + commandOptionsExpression: + commandsTypeNames[0] == null + ? null + : commandsTypeNames[0].commandOptionsExpression, + }, + ); +} +const parseModuleName = (hasteModuleName, moduleSpec, ast, parser) => { + const callExpressions = []; + visit(ast, { + CallExpression(node) { + if (isModuleRegistryCall(node)) { + callExpressions.push(node); + } + }, + }); + throwIfUnusedModuleInterfaceParserError( + hasteModuleName, + moduleSpec, + callExpressions, + ); + throwIfMoreThanOneModuleRegistryCalls( + hasteModuleName, + callExpressions, + callExpressions.length, + ); + const callExpression = callExpressions[0]; + const typeParameters = parser.callExpressionTypeParameters(callExpression); + const methodName = callExpression.callee.property.name; + throwIfWrongNumberOfCallExpressionArgs( + hasteModuleName, + callExpression, + methodName, + callExpression.arguments.length, + ); + throwIfIncorrectModuleRegistryCallArgument( + hasteModuleName, + callExpression.arguments[0], + methodName, + ); + const $moduleName = callExpression.arguments[0].value; + throwIfUntypedModule( + typeParameters, + hasteModuleName, + callExpression, + methodName, + $moduleName, + ); + throwIfIncorrectModuleRegistryCallTypeParameterParserError( + hasteModuleName, + typeParameters, + methodName, + $moduleName, + parser, + ); + return $moduleName; +}; +const buildModuleSchema = ( + hasteModuleName, + ast, + tryParse, + parser, + translateTypeAnnotation, +) => { + const language = parser.language(); + const types = parser.getTypes(ast); + const moduleSpecs = Object.values(types).filter(t => + parser.isModuleInterface(t), + ); + throwIfModuleInterfaceNotFound( + moduleSpecs.length, + hasteModuleName, + ast, + language, + ); + throwIfMoreThanOneModuleInterfaceParserError( + hasteModuleName, + moduleSpecs, + language, + ); + const _moduleSpecs = _slicedToArray(moduleSpecs, 1), + moduleSpec = _moduleSpecs[0]; + throwIfModuleInterfaceIsMisnamed(hasteModuleName, moduleSpec.id, language); + + // Parse Module Name + const moduleName = parseModuleName(hasteModuleName, moduleSpec, ast, parser); + + // Some module names use platform suffix to indicate platform-exclusive modules. + // Eventually this should be made explicit in the Flow type itself. + // Also check the hasteModuleName for platform suffix. + // Note: this shape is consistent with ComponentSchema. + const _verifyPlatforms = verifyPlatforms(hasteModuleName, moduleName), + cxxOnly = _verifyPlatforms.cxxOnly, + excludedPlatforms = _verifyPlatforms.excludedPlatforms; + const aliasMap = cxxOnly + ? getObjectTypeAnnotations( + hasteModuleName, + types, + tryParse, + translateTypeAnnotation, + parser, + ) + : {}; + const properties = + language === 'Flow' ? moduleSpec.body.properties : moduleSpec.body.body; + // $FlowFixMe[missing-type-arg] + const nativeModuleSchema = properties + .filter( + property => + property.type === 'ObjectTypeProperty' || + property.type === 'TSPropertySignature' || + property.type === 'TSMethodSignature', + ) + .map(property => { + var _property$value, _property$value2; + const enumMap = {}; + const isEventEmitter = + (property === null || + property === void 0 || + (_property$value = property.value) === null || + _property$value === void 0 + ? void 0 + : _property$value.type) === 'GenericTypeAnnotation' && + (property === null || + property === void 0 || + (_property$value2 = property.value) === null || + _property$value2 === void 0 || + (_property$value2 = _property$value2.id) === null || + _property$value2 === void 0 + ? void 0 + : _property$value2.name) === 'EventEmitter'; + return tryParse(() => ({ + aliasMap, + enumMap, + propertyShape: isEventEmitter + ? { + type: 'eventEmitter', + value: buildEventEmitterSchema( + hasteModuleName, + property, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + translateTypeAnnotation, + parser, + ), + } + : { + type: 'method', + value: buildPropertySchema( + hasteModuleName, + property, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + translateTypeAnnotation, + parser, + ), + }, + })); + }) + .filter(Boolean) + .reduce( + (moduleSchema, {enumMap, propertyShape}) => ({ + type: 'NativeModule', + aliasMap: _objectSpread( + _objectSpread({}, moduleSchema.aliasMap), + aliasMap, + ), + enumMap: _objectSpread( + _objectSpread({}, moduleSchema.enumMap), + enumMap, + ), + spec: { + eventEmitters: [...moduleSchema.spec.eventEmitters].concat( + propertyShape.type === 'eventEmitter' ? [propertyShape.value] : [], + ), + methods: [...moduleSchema.spec.methods].concat( + propertyShape.type === 'method' ? [propertyShape.value] : [], + ), + }, + moduleName: moduleSchema.moduleName, + excludedPlatforms: moduleSchema.excludedPlatforms, + }), + { + type: 'NativeModule', + aliasMap: {}, + enumMap: {}, + spec: { + eventEmitters: [], + methods: [], + }, + moduleName, + excludedPlatforms: + excludedPlatforms.length !== 0 ? [...excludedPlatforms] : undefined, + }, + ); + return { + type: 'NativeModule', + aliasMap: getSortedObject(nativeModuleSchema.aliasMap), + enumMap: getSortedObject(nativeModuleSchema.enumMap), + spec: { + eventEmitters: nativeModuleSchema.spec.eventEmitters.sort(), + methods: nativeModuleSchema.spec.methods.sort(), + }, + moduleName, + excludedPlatforms: nativeModuleSchema.excludedPlatforms, + }; +}; + +/** + * This function is used to find the type of a native component + * provided the default exports statement from generated AST. + * @param statement The statement to be parsed. + * @param foundConfigs The 'mutable' array of configs that have been found. + * @param parser The language parser to be used. + * @returns void + */ +function findNativeComponentType(statement, foundConfigs, parser) { + let declaration = statement.declaration; + + // codegenNativeComponent can be nested inside a cast + // expression so we need to go one level deeper + if ( + declaration.type === 'TSAsExpression' || + declaration.type === 'AsExpression' || + declaration.type === 'TypeCastExpression' + ) { + declaration = declaration.expression; + } + try { + if (declaration.callee.name === 'codegenNativeComponent') { + const typeArgumentParams = + parser.getTypeArgumentParamsFromDeclaration(declaration); + const funcArgumentParams = declaration.arguments; + const nativeComponentType = parser.getNativeComponentType( + typeArgumentParams, + funcArgumentParams, + ); + if (funcArgumentParams.length > 1) { + nativeComponentType.optionsExpression = funcArgumentParams[1]; + } + foundConfigs.push(nativeComponentType); + } + } catch (e) { + // ignore + } +} +function getCommandOptions(commandOptionsExpression) { + 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) { + 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; +} +function getCommandTypeNameAndOptionsExpression(namedExport, parser) { + let callExpression; + let calleeName; + try { + callExpression = namedExport.declaration.declarations[0].init; + calleeName = callExpression.callee.name; + } catch (e) { + return; + } + if (calleeName !== 'codegenNativeCommands') { + return; + } + if (callExpression.arguments.length !== 1) { + throw new Error( + 'codegenNativeCommands must be passed options including the supported commands', + ); + } + const typeArgumentParam = + parser.getTypeArgumentParamsFromDeclaration(callExpression)[0]; + if (!parser.isGenericTypeAnnotation(typeArgumentParam.type)) { + throw new Error( + "codegenNativeCommands doesn't support inline definitions. Specify a file local type alias", + ); + } + return { + commandTypeName: parser.getTypeAnnotationName(typeArgumentParam), + commandOptionsExpression: callExpression.arguments[0], + }; +} +function propertyNames(properties) { + return properties + .map(property => property && property.key && property.key.name) + .filter(Boolean); +} +function extendsForProp(prop, types, parser) { + const argument = parser.argumentForProp(prop); + if (!argument) { + console.log('null', prop); + } + const name = parser.nameForArgument(prop); + 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 buildPropSchema(property, types, parser) { + const getSchemaInfoFN = parser.getGetSchemaInfoFN(); + const info = getSchemaInfoFN(property, types); + if (info == null) { + return null; + } + const name = info.name, + optional = info.optional, + typeAnnotation = info.typeAnnotation, + defaultValue = info.defaultValue, + withNullDefault = info.withNullDefault; + const getTypeAnnotationFN = parser.getGetTypeAnnotationFN(); + return { + name, + optional, + typeAnnotation: getTypeAnnotationFN( + name, + typeAnnotation, + defaultValue, + withNullDefault, + types, + parser, + buildPropSchema, + ), + }; +} + +/* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's + * LTI update could not be added via codemod */ +function getEventArgument(argumentProps, parser, getPropertyType) { + return { + type: 'ObjectTypeAnnotation', + properties: argumentProps.map(member => + buildPropertiesForEvent(member, parser, getPropertyType), + ), + }; +} + +/* $FlowFixMe[signature-verification-failure] there's no flowtype for AST. + * TODO(T108222691): Use flow-types for @babel/parser */ +function findComponentConfig(ast, parser) { + const foundConfigs = []; + const defaultExports = ast.body.filter( + node => node.type === 'ExportDefaultDeclaration', + ); + defaultExports.forEach(statement => { + findNativeComponentType(statement, foundConfigs, parser); + }); + throwIfConfigNotfound(foundConfigs); + throwIfMoreThanOneConfig(foundConfigs); + const foundConfig = foundConfigs[0]; + const namedExports = ast.body.filter( + node => node.type === 'ExportNamedDeclaration', + ); + const commandsTypeNames = namedExports + .map(statement => getCommandTypeNameAndOptionsExpression(statement, parser)) + .filter(Boolean); + throwIfMoreThanOneCodegenNativecommands(commandsTypeNames); + return createComponentConfig(foundConfig, commandsTypeNames); +} + +// $FlowFixMe[signature-verification-failure] there's no flowtype for AST +function getCommandProperties(ast, parser) { + const _findComponentConfig = findComponentConfig(ast, parser), + commandTypeName = _findComponentConfig.commandTypeName, + commandOptionsExpression = _findComponentConfig.commandOptionsExpression; + if (commandTypeName == null) { + return []; + } + const types = parser.getTypes(ast); + const typeAlias = types[commandTypeName]; + throwIfTypeAliasIsNotInterface(typeAlias, parser); + const properties = parser.bodyProperties(typeAlias); + if (!properties) { + throw new Error( + `Failed to find type definition for "${commandTypeName}", please check that you have a valid codegen file`, + ); + } + const commandPropertyNames = propertyNames(properties); + const commandOptions = getCommandOptions(commandOptionsExpression); + if (commandOptions == null || commandOptions.supportedCommands == null) { + throw new Error( + 'codegenNativeCommands must be given an options object with supportedCommands array', + ); + } + if ( + commandOptions.supportedCommands.length !== commandPropertyNames.length || + !commandOptions.supportedCommands.every(supportedCommand => + commandPropertyNames.includes(supportedCommand), + ) + ) { + throw new Error( + `codegenNativeCommands expected the same supportedCommands specified in the ${commandTypeName} interface: ${commandPropertyNames.join( + ', ', + )}`, + ); + } + return properties; +} +function getTypeResolutionStatus(type, typeAnnotation, parser) { + return { + successful: true, + type, + name: parser.getTypeAnnotationName(typeAnnotation), + }; +} +function handleGenericTypeAnnotation( + typeAnnotation, + resolvedTypeAnnotation, + parser, +) { + let typeResolutionStatus; + let node; + switch (resolvedTypeAnnotation.type) { + case parser.typeAlias: { + typeResolutionStatus = getTypeResolutionStatus( + 'alias', + typeAnnotation, + parser, + ); + node = parser.nextNodeForTypeAlias(resolvedTypeAnnotation); + break; + } + case parser.enumDeclaration: { + typeResolutionStatus = getTypeResolutionStatus( + 'enum', + typeAnnotation, + parser, + ); + node = parser.nextNodeForEnum(resolvedTypeAnnotation); + break; + } + // parser.interfaceDeclaration is not used here because for flow it should fall through to default case and throw an error + case 'TSInterfaceDeclaration': { + typeResolutionStatus = getTypeResolutionStatus( + 'alias', + typeAnnotation, + parser, + ); + node = resolvedTypeAnnotation; + break; + } + default: { + throw new TypeError( + parser.genericTypeAnnotationErrorMessage(resolvedTypeAnnotation), + ); + } + } + return { + typeAnnotation: node, + typeResolutionStatus, + }; +} +function buildPropertiesForEvent(property, parser, getPropertyType) { + const name = property.key.name; + const optional = parser.isOptionalProperty(property); + const typeAnnotation = parser.getTypeAnnotationFromProperty(property); + return getPropertyType(name, optional, typeAnnotation, parser); +} +function verifyPropNotAlreadyDefined(props, needleProp) { + 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 handleEventHandler( + name, + typeAnnotation, + parser, + types, + findEventArgumentsAndType, +) { + const eventType = name === 'BubblingEventHandler' ? 'bubble' : 'direct'; + const paperTopLevelNameDeprecated = + parser.getPaperTopLevelNameDeprecated(typeAnnotation); + switch (typeAnnotation.typeParameters.params[0].type) { + case parser.nullLiteralTypeAnnotation: + case parser.undefinedLiteralTypeAnnotation: + return { + argumentProps: [], + bubblingType: eventType, + paperTopLevelNameDeprecated, + }; + default: + return findEventArgumentsAndType( + parser, + typeAnnotation.typeParameters.params[0], + types, + eventType, + paperTopLevelNameDeprecated, + ); + } +} +function emitBuildEventSchema( + paperTopLevelNameDeprecated, + name, + optional, + nonNullableBubblingType, + argument, +) { + if (paperTopLevelNameDeprecated != null) { + return { + name, + optional, + bubblingType: nonNullableBubblingType, + paperTopLevelNameDeprecated, + typeAnnotation: { + type: 'EventTypeAnnotation', + argument: argument, + }, + }; + } + return { + name, + optional, + bubblingType: nonNullableBubblingType, + typeAnnotation: { + type: 'EventTypeAnnotation', + argument: argument, + }, + }; +} +module.exports = { + wrapModuleSchema, + unwrapNullable, + wrapNullable, + assertGenericTypeAnnotationHasExactlyOneTypeParameter, + isObjectProperty, + parseObjectProperty, + translateFunctionTypeAnnotation, + buildPropertySchema, + buildSchemaFromConfigType, + buildSchema, + createComponentConfig, + parseModuleName, + buildModuleSchema, + findNativeComponentType, + propertyNames, + getCommandOptions, + getOptions, + getCommandTypeNameAndOptionsExpression, + extendsForProp, + buildPropSchema, + getEventArgument, + findComponentConfig, + getCommandProperties, + handleGenericTypeAnnotation, + getTypeResolutionStatus, + buildPropertiesForEvent, + verifyPropNotAlreadyDefined, + handleEventHandler, + emitBuildEventSchema, +}; diff --git a/packages/react-native-codegen/lib/parsers/parsers-commons.js.flow b/packages/react-native-codegen/lib/parsers/parsers-commons.js.flow new file mode 100644 index 00000000000000..443fc7dff717b8 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/parsers-commons.js.flow @@ -0,0 +1,1380 @@ +/** + * 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 { + EventTypeAnnotation, + EventTypeShape, + NamedShape, + NativeModuleAliasMap, + NativeModuleBaseTypeAnnotation, + NativeModuleEnumMap, + NativeModuleEventEmitterShape, + NativeModuleFunctionTypeAnnotation, + NativeModuleParamTypeAnnotation, + NativeModulePropertyShape, + NativeModuleSchema, + NativeModuleTypeAnnotation, + Nullable, + ObjectTypeAnnotation, + OptionsShape, + PropTypeAnnotation, + SchemaType, +} from '../CodegenSchema.js'; +import type {ParserType} from './errors'; +import type {Parser} from './parser'; +import type {ComponentSchemaBuilderConfig} from './schema.js'; +import type { + ParserErrorCapturer, + PropAST, + TypeDeclarationMap, + TypeResolutionStatus, +} from './utils'; + +const { + throwIfConfigNotfound, + throwIfEventEmitterEventTypeIsUnsupported, + throwIfEventEmitterTypeIsUnsupported, + throwIfIncorrectModuleRegistryCallArgument, + throwIfIncorrectModuleRegistryCallTypeParameterParserError, + throwIfModuleInterfaceIsMisnamed, + throwIfModuleInterfaceNotFound, + throwIfModuleTypeIsUnsupported, + throwIfMoreThanOneCodegenNativecommands, + throwIfMoreThanOneConfig, + throwIfMoreThanOneModuleInterfaceParserError, + throwIfMoreThanOneModuleRegistryCalls, + throwIfPropertyValueTypeIsUnsupported, + throwIfTypeAliasIsNotInterface, + throwIfUnsupportedFunctionParamTypeAnnotationParserError, + throwIfUnsupportedFunctionReturnTypeAnnotationParserError, + throwIfUntypedModule, + throwIfUnusedModuleInterfaceParserError, + throwIfWrongNumberOfCallExpressionArgs, +} = require('./error-utils'); +const { + MissingTypeParameterGenericParserError, + MoreThanOneTypeParameterGenericParserError, + UnnamedFunctionParamParserError, + UnsupportedObjectDirectRecursivePropertyParserError, +} = require('./errors'); +const { + createParserErrorCapturer, + extractNativeModuleName, + getConfigType, + getSortedObject, + isModuleRegistryCall, + verifyPlatforms, + visit, +} = require('./utils'); +const invariant = require('invariant'); + +export type CommandOptions = $ReadOnly<{ + supportedCommands: $ReadOnlyArray, +}>; + +// $FlowFixMe[unclear-type] TODO(T108222691): Use flow-types for @babel/parser +type OptionsAST = Object; + +type ExtendedPropResult = { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', +} | null; + +export type EventArgumentReturnType = { + argumentProps: ?$ReadOnlyArray<$FlowFixMe>, + paperTopLevelNameDeprecated: ?$FlowFixMe, + bubblingType: ?'direct' | 'bubble', +}; + +function wrapModuleSchema( + nativeModuleSchema: NativeModuleSchema, + hasteModuleName: string, +): SchemaType { + return { + modules: { + [hasteModuleName]: nativeModuleSchema, + }, + }; +} + +// $FlowFixMe[unsupported-variance-annotation] +function unwrapNullable<+T: NativeModuleTypeAnnotation>( + x: Nullable, +): [T, boolean] { + if (x.type === 'NullableTypeAnnotation') { + return [x.typeAnnotation, true]; + } + + return [x, false]; +} + +// $FlowFixMe[unsupported-variance-annotation] +function wrapNullable<+T: NativeModuleTypeAnnotation>( + nullable: boolean, + typeAnnotation: T, +): Nullable { + if (!nullable) { + return typeAnnotation; + } + + return { + type: 'NullableTypeAnnotation', + typeAnnotation, + }; +} + +function assertGenericTypeAnnotationHasExactlyOneTypeParameter( + moduleName: string, + /** + * TODO(T108222691): Use flow-types for @babel/parser + */ + typeAnnotation: $FlowFixMe, + parser: Parser, +) { + if (typeAnnotation.typeParameters == null) { + throw new MissingTypeParameterGenericParserError( + moduleName, + typeAnnotation, + parser, + ); + } + + const typeAnnotationType = parser.typeParameterInstantiation; + + invariant( + typeAnnotation.typeParameters.type === typeAnnotationType, + `assertGenericTypeAnnotationHasExactlyOneTypeParameter: Type parameters must be an AST node of type '${typeAnnotationType}'`, + ); + + if (typeAnnotation.typeParameters.params.length !== 1) { + throw new MoreThanOneTypeParameterGenericParserError( + moduleName, + typeAnnotation, + parser, + ); + } +} + +function isObjectProperty(property: $FlowFixMe, language: ParserType): boolean { + switch (language) { + case 'Flow': + return property.type === 'ObjectTypeProperty'; + case 'TypeScript': + return property.type === 'TSPropertySignature'; + default: + return false; + } +} + +function getObjectTypeAnnotations( + hasteModuleName: string, + types: TypeDeclarationMap, + tryParse: ParserErrorCapturer, + translateTypeAnnotation: $FlowFixMe, + parser: Parser, +): {...NativeModuleAliasMap} { + const aliasMap: {...NativeModuleAliasMap} = {}; + Object.entries(types).forEach(([key, value]) => { + const isTypeAlias = + value.type === 'TypeAlias' || value.type === 'TSTypeAliasDeclaration'; + if (!isTypeAlias) { + return; + } + const parent = parser.nextNodeForTypeAlias(value); + if ( + parent.type !== 'ObjectTypeAnnotation' && + parent.type !== 'TSTypeLiteral' + ) { + return; + } + const typeProperties = parser + .getAnnotatedElementProperties(value) + .map(prop => + parseObjectProperty( + parent, + prop, + hasteModuleName, + types, + aliasMap, + {}, // enumMap + tryParse, + true, // cxxOnly + prop?.optional || false, + translateTypeAnnotation, + parser, + ), + ); + aliasMap[key] = { + type: 'ObjectTypeAnnotation', + properties: typeProperties, + }; + }); + return aliasMap; +} + +function parseObjectProperty( + parentObject?: $FlowFixMe, + property: $FlowFixMe, + hasteModuleName: string, + types: TypeDeclarationMap, + aliasMap: {...NativeModuleAliasMap}, + enumMap: {...NativeModuleEnumMap}, + tryParse: ParserErrorCapturer, + cxxOnly: boolean, + nullable: boolean, + translateTypeAnnotation: $FlowFixMe, + parser: Parser, +): NamedShape> { + const language = parser.language(); + + const name = parser.getKeyName(property, hasteModuleName); + const {optional = false} = property; + const languageTypeAnnotation = + language === 'TypeScript' + ? property.typeAnnotation.typeAnnotation + : property.value; + + // Handle recursive types + if (parentObject) { + const propertyType = parser.getResolveTypeAnnotationFN()( + languageTypeAnnotation, + types, + parser, + ); + if ( + propertyType.typeResolutionStatus.successful === true && + propertyType.typeResolutionStatus.type === 'alias' && + (language === 'TypeScript' + ? parentObject.typeName && + parentObject.typeName.name === languageTypeAnnotation.typeName?.name + : parentObject.id && + parentObject.id.name === languageTypeAnnotation.id?.name) + ) { + if (!optional) { + throw new UnsupportedObjectDirectRecursivePropertyParserError( + name, + languageTypeAnnotation, + hasteModuleName, + ); + } + return { + name, + optional, + typeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: propertyType.typeResolutionStatus.name, + }, + }; + } + } + + // Handle non-recursive types + const [propertyTypeAnnotation, isPropertyNullable] = + unwrapNullable<$FlowFixMe>( + translateTypeAnnotation( + hasteModuleName, + languageTypeAnnotation, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + parser, + ), + ); + + if ( + (propertyTypeAnnotation.type === 'FunctionTypeAnnotation' && !cxxOnly) || + propertyTypeAnnotation.type === 'PromiseTypeAnnotation' || + propertyTypeAnnotation.type === 'VoidTypeAnnotation' + ) { + throwIfPropertyValueTypeIsUnsupported( + hasteModuleName, + languageTypeAnnotation, + property.key, + propertyTypeAnnotation.type, + ); + } + + return { + name, + optional, + typeAnnotation: wrapNullable(isPropertyNullable, propertyTypeAnnotation), + }; +} + +function translateFunctionTypeAnnotation( + hasteModuleName: string, + // TODO(T108222691): Use flow-types for @babel/parser + // TODO(T71778680): This is a FunctionTypeAnnotation. Type this. + functionTypeAnnotation: $FlowFixMe, + types: TypeDeclarationMap, + aliasMap: {...NativeModuleAliasMap}, + enumMap: {...NativeModuleEnumMap}, + tryParse: ParserErrorCapturer, + cxxOnly: boolean, + translateTypeAnnotation: $FlowFixMe, + parser: Parser, +): NativeModuleFunctionTypeAnnotation { + type Param = NamedShape>; + const params: Array = []; + + for (const param of parser.getFunctionTypeAnnotationParameters( + functionTypeAnnotation, + )) { + const parsedParam = tryParse(() => { + if (parser.getFunctionNameFromParameter(param) == null) { + throw new UnnamedFunctionParamParserError(param, hasteModuleName); + } + + const paramName = parser.getParameterName(param); + + const [paramTypeAnnotation, isParamTypeAnnotationNullable] = + unwrapNullable<$FlowFixMe>( + translateTypeAnnotation( + hasteModuleName, + parser.getParameterTypeAnnotation(param), + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + parser, + ), + ); + + if ( + paramTypeAnnotation.type === 'VoidTypeAnnotation' || + paramTypeAnnotation.type === 'PromiseTypeAnnotation' + ) { + return throwIfUnsupportedFunctionParamTypeAnnotationParserError( + hasteModuleName, + param.typeAnnotation, + paramName, + paramTypeAnnotation.type, + ); + } + + return { + name: paramName, + optional: Boolean(param.optional), + typeAnnotation: wrapNullable( + isParamTypeAnnotationNullable, + paramTypeAnnotation, + ), + }; + }); + + if (parsedParam != null) { + params.push(parsedParam); + } + } + + const [returnTypeAnnotation, isReturnTypeAnnotationNullable] = + unwrapNullable<$FlowFixMe>( + translateTypeAnnotation( + hasteModuleName, + parser.getFunctionTypeAnnotationReturnType(functionTypeAnnotation), + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + parser, + ), + ); + + throwIfUnsupportedFunctionReturnTypeAnnotationParserError( + hasteModuleName, + functionTypeAnnotation, + 'FunctionTypeAnnotation', + cxxOnly, + returnTypeAnnotation.type, + ); + + return { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: wrapNullable( + isReturnTypeAnnotationNullable, + returnTypeAnnotation, + ), + params, + }; +} + +function buildPropertySchema( + hasteModuleName: string, + // TODO(T108222691): [TS] Use flow-types for @babel/parser + // TODO(T71778680): [Flow] This is an ObjectTypeProperty containing either: + // - a FunctionTypeAnnotation or GenericTypeAnnotation + // - a NullableTypeAnnoation containing a FunctionTypeAnnotation or GenericTypeAnnotation + // Flow type this node + property: $FlowFixMe, + types: TypeDeclarationMap, + aliasMap: {...NativeModuleAliasMap}, + enumMap: {...NativeModuleEnumMap}, + tryParse: ParserErrorCapturer, + cxxOnly: boolean, + translateTypeAnnotation: $FlowFixMe, + parser: Parser, +): NativeModulePropertyShape { + let nullable: boolean = false; + let {key, value} = property; + const methodName: string = key.name; + + if (parser.language() === 'TypeScript') { + value = + property.type === 'TSMethodSignature' + ? property + : property.typeAnnotation; + } + + const resolveTypeAnnotationFN = parser.getResolveTypeAnnotationFN(); + ({nullable, typeAnnotation: value} = resolveTypeAnnotationFN( + value, + types, + parser, + )); + + throwIfModuleTypeIsUnsupported( + hasteModuleName, + property.value, + key.name, + value.type, + parser, + ); + + return { + name: methodName, + optional: Boolean(property.optional), + typeAnnotation: wrapNullable( + nullable, + translateFunctionTypeAnnotation( + hasteModuleName, + value, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + translateTypeAnnotation, + parser, + ), + ), + }; +} + +function buildEventEmitterSchema( + hasteModuleName: string, + // TODO(T108222691): [TS] Use flow-types for @babel/parser + // TODO(T71778680): [Flow] This is an ObjectTypeProperty containing either: + // - a FunctionTypeAnnotation or GenericTypeAnnotation + // - a NullableTypeAnnoation containing a FunctionTypeAnnotation or GenericTypeAnnotation + // Flow type this node + property: $FlowFixMe, + types: TypeDeclarationMap, + aliasMap: {...NativeModuleAliasMap}, + enumMap: {...NativeModuleEnumMap}, + tryParse: ParserErrorCapturer, + cxxOnly: boolean, + translateTypeAnnotation: $FlowFixMe, + parser: Parser, +): NativeModuleEventEmitterShape { + let {key, value} = property; + const eventemitterName: string = key.name; + + const resolveTypeAnnotationFN = parser.getResolveTypeAnnotationFN(); + const [typeAnnotation, typeAnnotationNullable] = unwrapNullable(value); + const typeAnnotationUntyped = + value.typeParameters.params.length === 1 && + value.typeParameters.params[0].type === 'ObjectTypeAnnotation' && + value.typeParameters.params[0].properties.length === 0; + + throwIfEventEmitterTypeIsUnsupported( + hasteModuleName, + key.name, + typeAnnotation.type, + parser, + typeAnnotationNullable, + typeAnnotationUntyped, + cxxOnly, + ); + const eventTypeResolutionStatus = resolveTypeAnnotationFN( + typeAnnotation.typeParameters.params[0], + types, + parser, + ); + throwIfEventEmitterEventTypeIsUnsupported( + hasteModuleName, + key.name, + eventTypeResolutionStatus.typeAnnotation, + parser, + eventTypeResolutionStatus.nullable, + ); + return { + name: eventemitterName, + optional: false, + typeAnnotation: { + type: 'EventEmitterTypeAnnotation', + typeAnnotation: {type: eventTypeResolutionStatus.typeAnnotation.type}, + }, + }; +} + +function buildSchemaFromConfigType( + configType: 'module' | 'component' | 'none', + filename: ?string, + ast: $FlowFixMe, + wrapComponentSchema: (config: ComponentSchemaBuilderConfig) => SchemaType, + buildComponentSchema: ( + ast: $FlowFixMe, + parser: Parser, + ) => ComponentSchemaBuilderConfig, + buildModuleSchema: ( + hasteModuleName: string, + ast: $FlowFixMe, + tryParse: ParserErrorCapturer, + parser: Parser, + translateTypeAnnotation: $FlowFixMe, + ) => NativeModuleSchema, + parser: Parser, + translateTypeAnnotation: $FlowFixMe, +): SchemaType { + switch (configType) { + case 'component': { + return wrapComponentSchema(buildComponentSchema(ast, parser)); + } + case 'module': { + if (filename === undefined || filename === null) { + throw new Error('Filepath expected while parasing a module'); + } + const nativeModuleName = extractNativeModuleName(filename); + + const [parsingErrors, tryParse] = createParserErrorCapturer(); + + const schema = tryParse(() => + buildModuleSchema( + nativeModuleName, + ast, + tryParse, + parser, + translateTypeAnnotation, + ), + ); + + if (parsingErrors.length > 0) { + /** + * TODO(T77968131): We have two options: + * - Throw the first error, but indicate there are more then one errors. + * - Display all errors, nicely formatted. + * + * For the time being, we're just throw the first error. + **/ + + throw parsingErrors[0]; + } + + invariant( + schema != null, + 'When there are no parsing errors, the schema should not be null', + ); + + return wrapModuleSchema(schema, nativeModuleName); + } + default: + return {modules: {}}; + } +} + +function buildSchema( + contents: string, + filename: ?string, + wrapComponentSchema: (config: ComponentSchemaBuilderConfig) => SchemaType, + buildComponentSchema: ( + ast: $FlowFixMe, + parser: Parser, + ) => ComponentSchemaBuilderConfig, + buildModuleSchema: ( + hasteModuleName: string, + ast: $FlowFixMe, + tryParse: ParserErrorCapturer, + parser: Parser, + translateTypeAnnotation: $FlowFixMe, + ) => NativeModuleSchema, + Visitor: ({isComponent: boolean, isModule: boolean}) => { + [type: string]: (node: $FlowFixMe) => void, + }, + parser: Parser, + translateTypeAnnotation: $FlowFixMe, +): SchemaType { + // Early return for non-Spec JavaScript files + if ( + !contents.includes('codegenNativeComponent') && + !contents.includes('TurboModule') + ) { + return {modules: {}}; + } + + const ast = parser.getAst(contents, filename); + const configType = getConfigType(ast, Visitor); + + return buildSchemaFromConfigType( + configType, + filename, + ast, + wrapComponentSchema, + buildComponentSchema, + buildModuleSchema, + parser, + translateTypeAnnotation, + ); +} + +function createComponentConfig( + foundConfig: $FlowFixMe, + commandsTypeNames: $FlowFixMe, +): $FlowFixMe { + return { + ...foundConfig, + commandTypeName: + commandsTypeNames[0] == null + ? null + : commandsTypeNames[0].commandTypeName, + commandOptionsExpression: + commandsTypeNames[0] == null + ? null + : commandsTypeNames[0].commandOptionsExpression, + }; +} + +const parseModuleName = ( + hasteModuleName: string, + moduleSpec: $FlowFixMe, + ast: $FlowFixMe, + parser: Parser, +): string => { + const callExpressions = []; + visit(ast, { + CallExpression(node) { + if (isModuleRegistryCall(node)) { + callExpressions.push(node); + } + }, + }); + + throwIfUnusedModuleInterfaceParserError( + hasteModuleName, + moduleSpec, + callExpressions, + ); + + throwIfMoreThanOneModuleRegistryCalls( + hasteModuleName, + callExpressions, + callExpressions.length, + ); + + const [callExpression] = callExpressions; + const typeParameters = parser.callExpressionTypeParameters(callExpression); + const methodName = callExpression.callee.property.name; + + throwIfWrongNumberOfCallExpressionArgs( + hasteModuleName, + callExpression, + methodName, + callExpression.arguments.length, + ); + + throwIfIncorrectModuleRegistryCallArgument( + hasteModuleName, + callExpression.arguments[0], + methodName, + ); + + const $moduleName = callExpression.arguments[0].value; + + throwIfUntypedModule( + typeParameters, + hasteModuleName, + callExpression, + methodName, + $moduleName, + ); + + throwIfIncorrectModuleRegistryCallTypeParameterParserError( + hasteModuleName, + typeParameters, + methodName, + $moduleName, + parser, + ); + + return $moduleName; +}; + +const buildModuleSchema = ( + hasteModuleName: string, + /** + * TODO(T71778680): Flow-type this node. + */ + ast: $FlowFixMe, + tryParse: ParserErrorCapturer, + parser: Parser, + translateTypeAnnotation: $FlowFixMe, +): NativeModuleSchema => { + const language = parser.language(); + const types = parser.getTypes(ast); + const moduleSpecs = (Object.values(types): $ReadOnlyArray<$FlowFixMe>).filter( + t => parser.isModuleInterface(t), + ); + + throwIfModuleInterfaceNotFound( + moduleSpecs.length, + hasteModuleName, + ast, + language, + ); + + throwIfMoreThanOneModuleInterfaceParserError( + hasteModuleName, + moduleSpecs, + language, + ); + + const [moduleSpec] = moduleSpecs; + + throwIfModuleInterfaceIsMisnamed(hasteModuleName, moduleSpec.id, language); + + // Parse Module Name + const moduleName = parseModuleName(hasteModuleName, moduleSpec, ast, parser); + + // Some module names use platform suffix to indicate platform-exclusive modules. + // Eventually this should be made explicit in the Flow type itself. + // Also check the hasteModuleName for platform suffix. + // Note: this shape is consistent with ComponentSchema. + const {cxxOnly, excludedPlatforms} = verifyPlatforms( + hasteModuleName, + moduleName, + ); + + const aliasMap: {...NativeModuleAliasMap} = cxxOnly + ? getObjectTypeAnnotations( + hasteModuleName, + types, + tryParse, + translateTypeAnnotation, + parser, + ) + : {}; + + const properties: $ReadOnlyArray<$FlowFixMe> = + language === 'Flow' ? moduleSpec.body.properties : moduleSpec.body.body; + + type PropertyShape = + | {type: 'eventEmitter', value: NativeModuleEventEmitterShape} + | {type: 'method', value: NativeModulePropertyShape}; + + // $FlowFixMe[missing-type-arg] + const nativeModuleSchema = properties + .filter( + property => + property.type === 'ObjectTypeProperty' || + property.type === 'TSPropertySignature' || + property.type === 'TSMethodSignature', + ) + .map(property => { + const enumMap: {...NativeModuleEnumMap} = {}; + const isEventEmitter = + property?.value?.type === 'GenericTypeAnnotation' && + property?.value?.id?.name === 'EventEmitter'; + return tryParse(() => ({ + aliasMap, + enumMap, + propertyShape: isEventEmitter + ? { + type: 'eventEmitter', + value: buildEventEmitterSchema( + hasteModuleName, + property, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + translateTypeAnnotation, + parser, + ), + } + : { + type: 'method', + value: buildPropertySchema( + hasteModuleName, + property, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + translateTypeAnnotation, + parser, + ), + }, + })); + }) + .filter(Boolean) + .reduce( + (moduleSchema: NativeModuleSchema, {enumMap, propertyShape}) => ({ + type: 'NativeModule', + aliasMap: {...moduleSchema.aliasMap, ...aliasMap}, + enumMap: {...moduleSchema.enumMap, ...enumMap}, + spec: { + eventEmitters: [...moduleSchema.spec.eventEmitters].concat( + propertyShape.type === 'eventEmitter' ? [propertyShape.value] : [], + ), + methods: [...moduleSchema.spec.methods].concat( + propertyShape.type === 'method' ? [propertyShape.value] : [], + ), + }, + moduleName: moduleSchema.moduleName, + excludedPlatforms: moduleSchema.excludedPlatforms, + }), + { + type: 'NativeModule', + aliasMap: {}, + enumMap: {}, + spec: {eventEmitters: [], methods: []}, + moduleName, + excludedPlatforms: + excludedPlatforms.length !== 0 ? [...excludedPlatforms] : undefined, + }, + ); + + return { + type: 'NativeModule', + aliasMap: getSortedObject(nativeModuleSchema.aliasMap), + enumMap: getSortedObject(nativeModuleSchema.enumMap), + spec: { + eventEmitters: nativeModuleSchema.spec.eventEmitters.sort(), + methods: nativeModuleSchema.spec.methods.sort(), + }, + moduleName, + excludedPlatforms: nativeModuleSchema.excludedPlatforms, + }; +}; + +/** + * This function is used to find the type of a native component + * provided the default exports statement from generated AST. + * @param statement The statement to be parsed. + * @param foundConfigs The 'mutable' array of configs that have been found. + * @param parser The language parser to be used. + * @returns void + */ +function findNativeComponentType( + statement: $FlowFixMe, + foundConfigs: Array<{[string]: string}>, + parser: Parser, +): void { + let declaration = statement.declaration; + + // codegenNativeComponent can be nested inside a cast + // expression so we need to go one level deeper + if ( + declaration.type === 'TSAsExpression' || + declaration.type === 'AsExpression' || + declaration.type === 'TypeCastExpression' + ) { + declaration = declaration.expression; + } + + try { + if (declaration.callee.name === 'codegenNativeComponent') { + const typeArgumentParams = + parser.getTypeArgumentParamsFromDeclaration(declaration); + const funcArgumentParams = declaration.arguments; + + const nativeComponentType: {[string]: string} = + parser.getNativeComponentType(typeArgumentParams, funcArgumentParams); + if (funcArgumentParams.length > 1) { + nativeComponentType.optionsExpression = funcArgumentParams[1]; + } + foundConfigs.push(nativeComponentType); + } + } catch (e) { + // ignore + } +} + +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; +} + +function getCommandTypeNameAndOptionsExpression( + namedExport: $FlowFixMe, + parser: Parser, +): { + commandOptionsExpression: OptionsAST, + commandTypeName: string, +} | void { + let callExpression; + let calleeName; + try { + callExpression = namedExport.declaration.declarations[0].init; + calleeName = callExpression.callee.name; + } catch (e) { + return; + } + + if (calleeName !== 'codegenNativeCommands') { + return; + } + + if (callExpression.arguments.length !== 1) { + throw new Error( + 'codegenNativeCommands must be passed options including the supported commands', + ); + } + + const typeArgumentParam = + parser.getTypeArgumentParamsFromDeclaration(callExpression)[0]; + + if (!parser.isGenericTypeAnnotation(typeArgumentParam.type)) { + throw new Error( + "codegenNativeCommands doesn't support inline definitions. Specify a file local type alias", + ); + } + + return { + commandTypeName: parser.getTypeAnnotationName(typeArgumentParam), + commandOptionsExpression: callExpression.arguments[0], + }; +} + +function propertyNames( + properties: $ReadOnlyArray<$FlowFixMe>, +): $ReadOnlyArray<$FlowFixMe> { + return properties + .map(property => property && property.key && property.key.name) + .filter(Boolean); +} + +function extendsForProp( + prop: PropAST, + types: TypeDeclarationMap, + parser: Parser, +): ExtendedPropResult { + const argument = parser.argumentForProp(prop); + + if (!argument) { + console.log('null', prop); + } + + const name = parser.nameForArgument(prop); + + 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 buildPropSchema( + property: PropAST, + types: TypeDeclarationMap, + parser: Parser, +): ?NamedShape { + const getSchemaInfoFN = parser.getGetSchemaInfoFN(); + const info = getSchemaInfoFN(property, types); + if (info == null) { + return null; + } + const {name, optional, typeAnnotation, defaultValue, withNullDefault} = info; + + const getTypeAnnotationFN = parser.getGetTypeAnnotationFN(); + + return { + name, + optional, + typeAnnotation: getTypeAnnotationFN( + name, + typeAnnotation, + defaultValue, + withNullDefault, + types, + parser, + buildPropSchema, + ), + }; +} + +/* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's + * LTI update could not be added via codemod */ +function getEventArgument( + argumentProps: PropAST, + parser: Parser, + getPropertyType: ( + name: $FlowFixMe, + optional: boolean, + typeAnnotation: $FlowFixMe, + parser: Parser, + ) => NamedShape, +): ObjectTypeAnnotation { + return { + type: 'ObjectTypeAnnotation', + properties: argumentProps.map(member => + buildPropertiesForEvent(member, parser, getPropertyType), + ), + }; +} + +/* $FlowFixMe[signature-verification-failure] there's no flowtype for AST. + * TODO(T108222691): Use flow-types for @babel/parser */ +function findComponentConfig(ast: $FlowFixMe, parser: Parser) { + const foundConfigs: Array<{[string]: string}> = []; + + const defaultExports = ast.body.filter( + node => node.type === 'ExportDefaultDeclaration', + ); + + defaultExports.forEach(statement => { + findNativeComponentType(statement, foundConfigs, parser); + }); + + throwIfConfigNotfound(foundConfigs); + throwIfMoreThanOneConfig(foundConfigs); + + const foundConfig = foundConfigs[0]; + + const namedExports = ast.body.filter( + node => node.type === 'ExportNamedDeclaration', + ); + + const commandsTypeNames = namedExports + .map(statement => getCommandTypeNameAndOptionsExpression(statement, parser)) + .filter(Boolean); + + throwIfMoreThanOneCodegenNativecommands(commandsTypeNames); + + return createComponentConfig(foundConfig, commandsTypeNames); +} + +// $FlowFixMe[signature-verification-failure] there's no flowtype for AST +function getCommandProperties(ast: $FlowFixMe, parser: Parser) { + const {commandTypeName, commandOptionsExpression} = findComponentConfig( + ast, + parser, + ); + + if (commandTypeName == null) { + return []; + } + const types = parser.getTypes(ast); + + const typeAlias = types[commandTypeName]; + + throwIfTypeAliasIsNotInterface(typeAlias, parser); + + const properties = parser.bodyProperties(typeAlias); + if (!properties) { + throw new Error( + `Failed to find type definition for "${commandTypeName}", please check that you have a valid codegen file`, + ); + } + + const commandPropertyNames = propertyNames(properties); + + const commandOptions = getCommandOptions(commandOptionsExpression); + + if (commandOptions == null || commandOptions.supportedCommands == null) { + throw new Error( + 'codegenNativeCommands must be given an options object with supportedCommands array', + ); + } + + if ( + commandOptions.supportedCommands.length !== commandPropertyNames.length || + !commandOptions.supportedCommands.every(supportedCommand => + commandPropertyNames.includes(supportedCommand), + ) + ) { + throw new Error( + `codegenNativeCommands expected the same supportedCommands specified in the ${commandTypeName} interface: ${commandPropertyNames.join( + ', ', + )}`, + ); + } + + return properties; +} + +function getTypeResolutionStatus( + type: 'alias' | 'enum', + typeAnnotation: $FlowFixMe, + parser: Parser, +): TypeResolutionStatus { + return { + successful: true, + type, + name: parser.getTypeAnnotationName(typeAnnotation), + }; +} + +function handleGenericTypeAnnotation( + typeAnnotation: $FlowFixMe, + resolvedTypeAnnotation: TypeDeclarationMap, + parser: Parser, +): { + typeAnnotation: $FlowFixMe, + typeResolutionStatus: TypeResolutionStatus, +} { + let typeResolutionStatus; + let node; + + switch (resolvedTypeAnnotation.type) { + case parser.typeAlias: { + typeResolutionStatus = getTypeResolutionStatus( + 'alias', + typeAnnotation, + parser, + ); + node = parser.nextNodeForTypeAlias(resolvedTypeAnnotation); + break; + } + case parser.enumDeclaration: { + typeResolutionStatus = getTypeResolutionStatus( + 'enum', + typeAnnotation, + parser, + ); + node = parser.nextNodeForEnum(resolvedTypeAnnotation); + break; + } + // parser.interfaceDeclaration is not used here because for flow it should fall through to default case and throw an error + case 'TSInterfaceDeclaration': { + typeResolutionStatus = getTypeResolutionStatus( + 'alias', + typeAnnotation, + parser, + ); + node = resolvedTypeAnnotation; + break; + } + default: { + throw new TypeError( + parser.genericTypeAnnotationErrorMessage(resolvedTypeAnnotation), + ); + } + } + + return { + typeAnnotation: node, + typeResolutionStatus, + }; +} + +function buildPropertiesForEvent( + property: $FlowFixMe, + parser: Parser, + getPropertyType: ( + name: $FlowFixMe, + optional: boolean, + typeAnnotation: $FlowFixMe, + parser: Parser, + ) => NamedShape, +): NamedShape { + const name = property.key.name; + const optional = parser.isOptionalProperty(property); + const typeAnnotation = parser.getTypeAnnotationFromProperty(property); + + return getPropertyType(name, optional, typeAnnotation, parser); +} + +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 handleEventHandler( + name: 'BubblingEventHandler' | 'DirectEventHandler', + typeAnnotation: $FlowFixMe, + parser: Parser, + types: TypeDeclarationMap, + findEventArgumentsAndType: ( + parser: Parser, + typeAnnotation: $FlowFixMe, + types: TypeDeclarationMap, + bubblingType: void | 'direct' | 'bubble', + paperName: ?$FlowFixMe, + ) => EventArgumentReturnType, +): EventArgumentReturnType { + const eventType = name === 'BubblingEventHandler' ? 'bubble' : 'direct'; + const paperTopLevelNameDeprecated = + parser.getPaperTopLevelNameDeprecated(typeAnnotation); + + switch (typeAnnotation.typeParameters.params[0].type) { + case parser.nullLiteralTypeAnnotation: + case parser.undefinedLiteralTypeAnnotation: + return { + argumentProps: [], + bubblingType: eventType, + paperTopLevelNameDeprecated, + }; + default: + return findEventArgumentsAndType( + parser, + typeAnnotation.typeParameters.params[0], + types, + eventType, + paperTopLevelNameDeprecated, + ); + } +} + +function emitBuildEventSchema( + paperTopLevelNameDeprecated: $FlowFixMe, + name: $FlowFixMe, + optional: $FlowFixMe, + nonNullableBubblingType: 'direct' | 'bubble', + argument: ObjectTypeAnnotation, +): ?EventTypeShape { + if (paperTopLevelNameDeprecated != null) { + return { + name, + optional, + bubblingType: nonNullableBubblingType, + paperTopLevelNameDeprecated, + typeAnnotation: { + type: 'EventTypeAnnotation', + argument: argument, + }, + }; + } + + return { + name, + optional, + bubblingType: nonNullableBubblingType, + typeAnnotation: { + type: 'EventTypeAnnotation', + argument: argument, + }, + }; +} + +module.exports = { + wrapModuleSchema, + unwrapNullable, + wrapNullable, + assertGenericTypeAnnotationHasExactlyOneTypeParameter, + isObjectProperty, + parseObjectProperty, + translateFunctionTypeAnnotation, + buildPropertySchema, + buildSchemaFromConfigType, + buildSchema, + createComponentConfig, + parseModuleName, + buildModuleSchema, + findNativeComponentType, + propertyNames, + getCommandOptions, + getOptions, + getCommandTypeNameAndOptionsExpression, + extendsForProp, + buildPropSchema, + getEventArgument, + findComponentConfig, + getCommandProperties, + handleGenericTypeAnnotation, + getTypeResolutionStatus, + buildPropertiesForEvent, + verifyPropNotAlreadyDefined, + handleEventHandler, + emitBuildEventSchema, +}; diff --git a/packages/react-native-codegen/lib/parsers/parsers-primitives.js b/packages/react-native-codegen/lib/parsers/parsers-primitives.js new file mode 100644 index 00000000000000..2f9ce3c70a6363 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/parsers-primitives.js @@ -0,0 +1,670 @@ +/** + * 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'; + +function _slicedToArray(r, e) { + return ( + _arrayWithHoles(r) || + _iterableToArrayLimit(r, e) || + _unsupportedIterableToArray(r, e) || + _nonIterableRest() + ); +} +function _nonIterableRest() { + throw new TypeError( + 'Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.', + ); +} +function _unsupportedIterableToArray(r, a) { + if (r) { + if ('string' == typeof r) return _arrayLikeToArray(r, a); + var t = {}.toString.call(r).slice(8, -1); + return ( + 'Object' === t && r.constructor && (t = r.constructor.name), + 'Map' === t || 'Set' === t + ? Array.from(r) + : 'Arguments' === t || + /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) + ? _arrayLikeToArray(r, a) + : void 0 + ); + } +} +function _arrayLikeToArray(r, a) { + (null == a || a > r.length) && (a = r.length); + for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; + return n; +} +function _iterableToArrayLimit(r, l) { + var t = + null == r + ? null + : ('undefined' != typeof Symbol && r[Symbol.iterator]) || r['@@iterator']; + if (null != t) { + var e, + n, + i, + u, + a = [], + f = !0, + o = !1; + try { + if (((i = (t = t.call(r)).next), 0 === l)) { + if (Object(t) !== t) return; + f = !1; + } else + for ( + ; + !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); + f = !0 + ); + } catch (r) { + (o = !0), (n = r); + } finally { + try { + if (!f && null != t.return && ((u = t.return()), Object(u) !== u)) + return; + } finally { + if (o) throw n; + } + } + return a; + } +} +function _arrayWithHoles(r) { + if (Array.isArray(r)) return r; +} +const _require = require('./error-utils'), + throwIfArrayElementTypeAnnotationIsUnsupported = + _require.throwIfArrayElementTypeAnnotationIsUnsupported, + throwIfPartialNotAnnotatingTypeParameter = + _require.throwIfPartialNotAnnotatingTypeParameter, + throwIfPartialWithMoreParameter = _require.throwIfPartialWithMoreParameter; +const _require2 = require('./errors'), + ParserError = _require2.ParserError, + UnsupportedTypeAnnotationParserError = + _require2.UnsupportedTypeAnnotationParserError, + UnsupportedUnionTypeAnnotationParserError = + _require2.UnsupportedUnionTypeAnnotationParserError; +const _require3 = require('./parsers-commons'), + assertGenericTypeAnnotationHasExactlyOneTypeParameter = + _require3.assertGenericTypeAnnotationHasExactlyOneTypeParameter, + translateFunctionTypeAnnotation = _require3.translateFunctionTypeAnnotation, + unwrapNullable = _require3.unwrapNullable, + wrapNullable = _require3.wrapNullable; +const _require4 = require('./parsers-utils'), + nullGuard = _require4.nullGuard; +const _require5 = require('./utils'), + isModuleRegistryCall = _require5.isModuleRegistryCall; +function emitBoolean(nullable) { + return wrapNullable(nullable, { + type: 'BooleanTypeAnnotation', + }); +} +function emitInt32(nullable) { + return wrapNullable(nullable, { + type: 'Int32TypeAnnotation', + }); +} +function emitInt32Prop(name, optional) { + return { + name, + optional, + typeAnnotation: { + type: 'Int32TypeAnnotation', + }, + }; +} +function emitNumber(nullable) { + return wrapNullable(nullable, { + type: 'NumberTypeAnnotation', + }); +} +function emitRootTag(nullable) { + return wrapNullable(nullable, { + type: 'ReservedTypeAnnotation', + name: 'RootTag', + }); +} +function emitDouble(nullable) { + return wrapNullable(nullable, { + type: 'DoubleTypeAnnotation', + }); +} +function emitDoubleProp(name, optional) { + return { + name, + optional, + typeAnnotation: { + type: 'DoubleTypeAnnotation', + }, + }; +} +function emitVoid(nullable) { + return wrapNullable(nullable, { + type: 'VoidTypeAnnotation', + }); +} +function emitStringish(nullable) { + return wrapNullable(nullable, { + type: 'StringTypeAnnotation', + }); +} +function emitFunction( + nullable, + hasteModuleName, + typeAnnotation, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + translateTypeAnnotation, + parser, +) { + const translateFunctionTypeAnnotationValue = translateFunctionTypeAnnotation( + hasteModuleName, + typeAnnotation, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + translateTypeAnnotation, + parser, + ); + return wrapNullable(nullable, translateFunctionTypeAnnotationValue); +} +function emitMixed(nullable) { + return wrapNullable(nullable, { + type: 'MixedTypeAnnotation', + }); +} +function emitString(nullable) { + return wrapNullable(nullable, { + type: 'StringTypeAnnotation', + }); +} +function emitStringProp(name, optional) { + return { + name, + optional, + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }; +} +function typeAliasResolution( + typeResolution, + objectTypeAnnotation, + aliasMap, + nullable, +) { + if (!typeResolution.successful) { + return wrapNullable(nullable, objectTypeAnnotation); + } + + /** + * All aliases RHS are required. + */ + aliasMap[typeResolution.name] = objectTypeAnnotation; + + /** + * Nullability of type aliases is transitive. + * + * Consider this case: + * + * type Animal = ?{ + * name: string, + * }; + * + * type B = Animal + * + * export interface Spec extends TurboModule { + * +greet: (animal: B) => void; + * } + * + * In this case, we follow B to Animal, and then Animal to ?{name: string}. + * + * We: + * 1. Replace `+greet: (animal: B) => void;` with `+greet: (animal: ?Animal) => void;`, + * 2. Pretend that Animal = {name: string}. + * + * Why do we do this? + * 1. In ObjC, we need to generate a struct called Animal, not B. + * 2. This design is simpler than managing nullability within both the type alias usage, and the type alias RHS. + * 3. What does it mean for a C++ struct, which is what this type alias RHS will generate, to be nullable? ¯\_(ツ)_/¯ + * Nullability is a concept that only makes sense when talking about instances (i.e: usages) of the C++ structs. + * Hence, it's better to manage nullability within the actual TypeAliasTypeAnnotation nodes, and not the + * associated ObjectTypeAnnotations. + */ + return wrapNullable(nullable, { + type: 'TypeAliasTypeAnnotation', + name: typeResolution.name, + }); +} +function typeEnumResolution( + typeAnnotation, + typeResolution, + nullable, + hasteModuleName, + enumMap, + parser, +) { + if (!typeResolution.successful || typeResolution.type !== 'enum') { + throw new UnsupportedTypeAnnotationParserError( + hasteModuleName, + typeAnnotation, + parser.language(), + ); + } + const enumName = typeResolution.name; + const enumMemberType = parser.parseEnumMembersType(typeAnnotation); + try { + parser.validateEnumMembersSupported(typeAnnotation, enumMemberType); + } catch (e) { + if (e instanceof Error) { + throw new ParserError( + hasteModuleName, + typeAnnotation, + `Failed parsing the enum ${enumName} in ${hasteModuleName} with the error: ${e.message}`, + ); + } else { + throw e; + } + } + const enumMembers = parser.parseEnumMembers(typeAnnotation); + enumMap[enumName] = { + name: enumName, + type: 'EnumDeclarationWithMembers', + memberType: enumMemberType, + members: enumMembers, + }; + return wrapNullable(nullable, { + name: enumName, + type: 'EnumDeclaration', + memberType: enumMemberType, + }); +} +function emitPromise( + hasteModuleName, + typeAnnotation, + parser, + nullable, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + translateTypeAnnotation, +) { + assertGenericTypeAnnotationHasExactlyOneTypeParameter( + hasteModuleName, + typeAnnotation, + parser, + ); + const elementType = typeAnnotation.typeParameters.params[0]; + if ( + elementType.type === 'ExistsTypeAnnotation' || + elementType.type === 'EmptyTypeAnnotation' + ) { + return wrapNullable(nullable, { + type: 'PromiseTypeAnnotation', + }); + } else { + try { + return wrapNullable(nullable, { + type: 'PromiseTypeAnnotation', + elementType: translateTypeAnnotation( + hasteModuleName, + typeAnnotation.typeParameters.params[0], + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + parser, + ), + }); + } catch { + return wrapNullable(nullable, { + type: 'PromiseTypeAnnotation', + }); + } + } +} +function emitGenericObject(nullable) { + return wrapNullable(nullable, { + type: 'GenericObjectTypeAnnotation', + }); +} +function emitDictionary(nullable, valueType) { + return wrapNullable(nullable, { + type: 'GenericObjectTypeAnnotation', + dictionaryValueType: valueType, + }); +} +function emitObject(nullable, properties) { + return wrapNullable(nullable, { + type: 'ObjectTypeAnnotation', + properties, + }); +} +function emitFloat(nullable) { + return wrapNullable(nullable, { + type: 'FloatTypeAnnotation', + }); +} +function emitFloatProp(name, optional) { + return { + name, + optional, + typeAnnotation: { + type: 'FloatTypeAnnotation', + }, + }; +} +function emitUnion(nullable, hasteModuleName, typeAnnotation, parser) { + const unionTypes = parser.remapUnionTypeAnnotationMemberNames( + typeAnnotation.types, + ); + + // Only support unionTypes of the same kind + if (unionTypes.length > 1) { + throw new UnsupportedUnionTypeAnnotationParserError( + hasteModuleName, + typeAnnotation, + unionTypes, + ); + } + return wrapNullable(nullable, { + type: 'UnionTypeAnnotation', + memberType: unionTypes[0], + }); +} +function translateArrayTypeAnnotation( + hasteModuleName, + types, + aliasMap, + enumMap, + cxxOnly, + arrayType, + elementType, + nullable, + translateTypeAnnotation, + parser, +) { + try { + /** + * TODO(T72031674): Migrate all our NativeModule specs to not use + * invalid Array ElementTypes. Then, make the elementType a required + * parameter. + */ + const _unwrapNullable = unwrapNullable( + translateTypeAnnotation( + hasteModuleName, + elementType, + types, + aliasMap, + enumMap, + /** + * TODO(T72031674): Ensure that all ParsingErrors that are thrown + * while parsing the array element don't get captured and collected. + * Why? If we detect any parsing error while parsing the element, + * we should default it to null down the line, here. This is + * the correct behaviour until we migrate all our NativeModule specs + * to be parseable. + */ + nullGuard, + cxxOnly, + parser, + ), + ), + _unwrapNullable2 = _slicedToArray(_unwrapNullable, 2), + _elementType = _unwrapNullable2[0], + isElementTypeNullable = _unwrapNullable2[1]; + throwIfArrayElementTypeAnnotationIsUnsupported( + hasteModuleName, + elementType, + arrayType, + _elementType.type, + ); + return wrapNullable(nullable, { + type: 'ArrayTypeAnnotation', + // $FlowFixMe[incompatible-call] + elementType: wrapNullable(isElementTypeNullable, _elementType), + }); + } catch (ex) { + return wrapNullable(nullable, { + type: 'ArrayTypeAnnotation', + }); + } +} +function emitArrayType( + hasteModuleName, + typeAnnotation, + parser, + types, + aliasMap, + enumMap, + cxxOnly, + nullable, + translateTypeAnnotation, +) { + assertGenericTypeAnnotationHasExactlyOneTypeParameter( + hasteModuleName, + typeAnnotation, + parser, + ); + return translateArrayTypeAnnotation( + hasteModuleName, + types, + aliasMap, + enumMap, + cxxOnly, + typeAnnotation.type, + typeAnnotation.typeParameters.params[0], + nullable, + translateTypeAnnotation, + parser, + ); +} +function Visitor(infoMap) { + return { + CallExpression(node) { + if ( + node.callee.type === 'Identifier' && + node.callee.name === 'codegenNativeComponent' + ) { + infoMap.isComponent = true; + } + if (isModuleRegistryCall(node)) { + infoMap.isModule = true; + } + }, + InterfaceExtends(node) { + if (node.id.name === 'TurboModule') { + infoMap.isModule = true; + } + }, + TSInterfaceDeclaration(node) { + if ( + Array.isArray(node.extends) && + node.extends.some( + extension => extension.expression.name === 'TurboModule', + ) + ) { + infoMap.isModule = true; + } + }, + }; +} +function emitPartial( + nullable, + hasteModuleName, + typeAnnotation, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + parser, +) { + throwIfPartialWithMoreParameter(typeAnnotation); + throwIfPartialNotAnnotatingTypeParameter(typeAnnotation, types, parser); + const annotatedElement = parser.extractAnnotatedElement( + typeAnnotation, + types, + ); + const annotatedElementProperties = + parser.getAnnotatedElementProperties(annotatedElement); + const partialProperties = parser.computePartialProperties( + annotatedElementProperties, + hasteModuleName, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + ); + return emitObject(nullable, partialProperties); +} +function emitCommonTypes( + hasteModuleName, + types, + typeAnnotation, + aliasMap, + enumMap, + tryParse, + cxxOnly, + nullable, + parser, +) { + const typeMap = { + Stringish: emitStringish, + Int32: emitInt32, + Double: emitDouble, + Float: emitFloat, + UnsafeObject: emitGenericObject, + Object: emitGenericObject, + $Partial: emitPartial, + Partial: emitPartial, + BooleanTypeAnnotation: emitBoolean, + NumberTypeAnnotation: emitNumber, + VoidTypeAnnotation: emitVoid, + StringTypeAnnotation: emitString, + MixedTypeAnnotation: cxxOnly ? emitMixed : emitGenericObject, + }; + const typeAnnotationName = parser.convertKeywordToTypeAnnotation( + typeAnnotation.type, + ); + + // $FlowFixMe[invalid-computed-prop] + const simpleEmitter = typeMap[typeAnnotationName]; + if (simpleEmitter) { + return simpleEmitter(nullable); + } + const genericTypeAnnotationName = + parser.getTypeAnnotationName(typeAnnotation); + + // $FlowFixMe[invalid-computed-prop] + const emitter = typeMap[genericTypeAnnotationName]; + if (!emitter) { + return null; + } + return emitter( + nullable, + hasteModuleName, + typeAnnotation, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + parser, + ); +} +function emitBoolProp(name, optional) { + return { + name, + optional, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }; +} +function emitMixedProp(name, optional) { + return { + name, + optional, + typeAnnotation: { + type: 'MixedTypeAnnotation', + }, + }; +} +function emitObjectProp( + name, + optional, + parser, + typeAnnotation, + extractArrayElementType, +) { + return { + name, + optional, + typeAnnotation: extractArrayElementType(typeAnnotation, name, parser), + }; +} +function emitUnionProp(name, optional, parser, typeAnnotation) { + return { + name, + optional, + typeAnnotation: { + type: 'StringEnumTypeAnnotation', + options: typeAnnotation.types.map(option => + parser.getLiteralValue(option), + ), + }, + }; +} +module.exports = { + emitArrayType, + emitBoolean, + emitBoolProp, + emitDouble, + emitDoubleProp, + emitFloat, + emitFloatProp, + emitFunction, + emitInt32, + emitInt32Prop, + emitMixedProp, + emitNumber, + emitGenericObject, + emitDictionary, + emitObject, + emitPromise, + emitRootTag, + emitVoid, + emitString, + emitStringish, + emitStringProp, + emitMixed, + emitUnion, + emitPartial, + emitCommonTypes, + typeAliasResolution, + typeEnumResolution, + translateArrayTypeAnnotation, + Visitor, + emitObjectProp, + emitUnionProp, +}; diff --git a/packages/react-native-codegen/lib/parsers/parsers-primitives.js.flow b/packages/react-native-codegen/lib/parsers/parsers-primitives.js.flow new file mode 100644 index 00000000000000..ca967120cb0c00 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/parsers-primitives.js.flow @@ -0,0 +1,728 @@ +/** + * 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 { + BooleanTypeAnnotation, + DoubleTypeAnnotation, + EventTypeAnnotation, + Int32TypeAnnotation, + NamedShape, + NativeModuleAliasMap, + NativeModuleBaseTypeAnnotation, + NativeModuleEnumDeclaration, + NativeModuleEnumMap, + NativeModuleFloatTypeAnnotation, + NativeModuleFunctionTypeAnnotation, + NativeModuleGenericObjectTypeAnnotation, + NativeModuleMixedTypeAnnotation, + NativeModuleNumberTypeAnnotation, + NativeModuleObjectTypeAnnotation, + NativeModulePromiseTypeAnnotation, + NativeModuleTypeAliasTypeAnnotation, + NativeModuleTypeAnnotation, + NativeModuleUnionTypeAnnotation, + Nullable, + ObjectTypeAnnotation, + ReservedTypeAnnotation, + StringTypeAnnotation, + VoidTypeAnnotation, +} from '../CodegenSchema'; +import type {Parser} from './parser'; +import type { + ParserErrorCapturer, + TypeDeclarationMap, + TypeResolutionStatus, +} from './utils'; + +const { + throwIfArrayElementTypeAnnotationIsUnsupported, + throwIfPartialNotAnnotatingTypeParameter, + throwIfPartialWithMoreParameter, +} = require('./error-utils'); +const { + ParserError, + UnsupportedTypeAnnotationParserError, + UnsupportedUnionTypeAnnotationParserError, +} = require('./errors'); +const { + assertGenericTypeAnnotationHasExactlyOneTypeParameter, + translateFunctionTypeAnnotation, + unwrapNullable, + wrapNullable, +} = require('./parsers-commons'); +const {nullGuard} = require('./parsers-utils'); +const {isModuleRegistryCall} = require('./utils'); + +function emitBoolean(nullable: boolean): Nullable { + return wrapNullable(nullable, { + type: 'BooleanTypeAnnotation', + }); +} + +function emitInt32(nullable: boolean): Nullable { + return wrapNullable(nullable, { + type: 'Int32TypeAnnotation', + }); +} + +function emitInt32Prop( + name: string, + optional: boolean, +): NamedShape { + return { + name, + optional, + typeAnnotation: { + type: 'Int32TypeAnnotation', + }, + }; +} + +function emitNumber( + nullable: boolean, +): Nullable { + return wrapNullable(nullable, { + type: 'NumberTypeAnnotation', + }); +} + +function emitRootTag(nullable: boolean): Nullable { + return wrapNullable(nullable, { + type: 'ReservedTypeAnnotation', + name: 'RootTag', + }); +} + +function emitDouble(nullable: boolean): Nullable { + return wrapNullable(nullable, { + type: 'DoubleTypeAnnotation', + }); +} + +function emitDoubleProp( + name: string, + optional: boolean, +): NamedShape { + return { + name, + optional, + typeAnnotation: { + type: 'DoubleTypeAnnotation', + }, + }; +} + +function emitVoid(nullable: boolean): Nullable { + return wrapNullable(nullable, { + type: 'VoidTypeAnnotation', + }); +} + +function emitStringish(nullable: boolean): Nullable { + return wrapNullable(nullable, { + type: 'StringTypeAnnotation', + }); +} + +function emitFunction( + nullable: boolean, + hasteModuleName: string, + typeAnnotation: $FlowFixMe, + types: TypeDeclarationMap, + aliasMap: {...NativeModuleAliasMap}, + enumMap: {...NativeModuleEnumMap}, + tryParse: ParserErrorCapturer, + cxxOnly: boolean, + translateTypeAnnotation: $FlowFixMe, + parser: Parser, +): Nullable { + const translateFunctionTypeAnnotationValue: NativeModuleFunctionTypeAnnotation = + translateFunctionTypeAnnotation( + hasteModuleName, + typeAnnotation, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + translateTypeAnnotation, + parser, + ); + return wrapNullable(nullable, translateFunctionTypeAnnotationValue); +} + +function emitMixed( + nullable: boolean, +): Nullable { + return wrapNullable(nullable, { + type: 'MixedTypeAnnotation', + }); +} + +function emitString(nullable: boolean): Nullable { + return wrapNullable(nullable, { + type: 'StringTypeAnnotation', + }); +} + +function emitStringProp( + name: string, + optional: boolean, +): NamedShape { + return { + name, + optional, + typeAnnotation: { + type: 'StringTypeAnnotation', + }, + }; +} + +function typeAliasResolution( + typeResolution: TypeResolutionStatus, + objectTypeAnnotation: ObjectTypeAnnotation< + Nullable, + >, + aliasMap: {...NativeModuleAliasMap}, + nullable: boolean, +): + | Nullable + | Nullable>> { + if (!typeResolution.successful) { + return wrapNullable(nullable, objectTypeAnnotation); + } + + /** + * All aliases RHS are required. + */ + aliasMap[typeResolution.name] = objectTypeAnnotation; + + /** + * Nullability of type aliases is transitive. + * + * Consider this case: + * + * type Animal = ?{ + * name: string, + * }; + * + * type B = Animal + * + * export interface Spec extends TurboModule { + * +greet: (animal: B) => void; + * } + * + * In this case, we follow B to Animal, and then Animal to ?{name: string}. + * + * We: + * 1. Replace `+greet: (animal: B) => void;` with `+greet: (animal: ?Animal) => void;`, + * 2. Pretend that Animal = {name: string}. + * + * Why do we do this? + * 1. In ObjC, we need to generate a struct called Animal, not B. + * 2. This design is simpler than managing nullability within both the type alias usage, and the type alias RHS. + * 3. What does it mean for a C++ struct, which is what this type alias RHS will generate, to be nullable? ¯\_(ツ)_/¯ + * Nullability is a concept that only makes sense when talking about instances (i.e: usages) of the C++ structs. + * Hence, it's better to manage nullability within the actual TypeAliasTypeAnnotation nodes, and not the + * associated ObjectTypeAnnotations. + */ + return wrapNullable(nullable, { + type: 'TypeAliasTypeAnnotation', + name: typeResolution.name, + }); +} + +function typeEnumResolution( + typeAnnotation: $FlowFixMe, + typeResolution: TypeResolutionStatus, + nullable: boolean, + hasteModuleName: string, + enumMap: {...NativeModuleEnumMap}, + parser: Parser, +): Nullable { + if (!typeResolution.successful || typeResolution.type !== 'enum') { + throw new UnsupportedTypeAnnotationParserError( + hasteModuleName, + typeAnnotation, + parser.language(), + ); + } + + const enumName = typeResolution.name; + + const enumMemberType = parser.parseEnumMembersType(typeAnnotation); + + try { + parser.validateEnumMembersSupported(typeAnnotation, enumMemberType); + } catch (e) { + if (e instanceof Error) { + throw new ParserError( + hasteModuleName, + typeAnnotation, + `Failed parsing the enum ${enumName} in ${hasteModuleName} with the error: ${e.message}`, + ); + } else { + throw e; + } + } + + const enumMembers = parser.parseEnumMembers(typeAnnotation); + + enumMap[enumName] = { + name: enumName, + type: 'EnumDeclarationWithMembers', + memberType: enumMemberType, + members: enumMembers, + }; + + return wrapNullable(nullable, { + name: enumName, + type: 'EnumDeclaration', + memberType: enumMemberType, + }); +} + +function emitPromise( + hasteModuleName: string, + typeAnnotation: $FlowFixMe, + parser: Parser, + nullable: boolean, + types: TypeDeclarationMap, + aliasMap: {...NativeModuleAliasMap}, + enumMap: {...NativeModuleEnumMap}, + tryParse: ParserErrorCapturer, + cxxOnly: boolean, + translateTypeAnnotation: $FlowFixMe, +): Nullable { + assertGenericTypeAnnotationHasExactlyOneTypeParameter( + hasteModuleName, + typeAnnotation, + parser, + ); + + const elementType = typeAnnotation.typeParameters.params[0]; + if ( + elementType.type === 'ExistsTypeAnnotation' || + elementType.type === 'EmptyTypeAnnotation' + ) { + return wrapNullable(nullable, { + type: 'PromiseTypeAnnotation', + }); + } else { + try { + return wrapNullable(nullable, { + type: 'PromiseTypeAnnotation', + elementType: translateTypeAnnotation( + hasteModuleName, + typeAnnotation.typeParameters.params[0], + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + parser, + ), + }); + } catch { + return wrapNullable(nullable, { + type: 'PromiseTypeAnnotation', + }); + } + } +} + +function emitGenericObject( + nullable: boolean, +): Nullable { + return wrapNullable(nullable, { + type: 'GenericObjectTypeAnnotation', + }); +} + +function emitDictionary( + nullable: boolean, + valueType: Nullable, +): Nullable { + return wrapNullable(nullable, { + type: 'GenericObjectTypeAnnotation', + dictionaryValueType: valueType, + }); +} + +function emitObject( + nullable: boolean, + properties: Array<$FlowFixMe>, +): Nullable { + return wrapNullable(nullable, { + type: 'ObjectTypeAnnotation', + properties, + }); +} + +function emitFloat( + nullable: boolean, +): Nullable { + return wrapNullable(nullable, { + type: 'FloatTypeAnnotation', + }); +} + +function emitFloatProp( + name: string, + optional: boolean, +): NamedShape { + return { + name, + optional, + typeAnnotation: { + type: 'FloatTypeAnnotation', + }, + }; +} + +function emitUnion( + nullable: boolean, + hasteModuleName: string, + typeAnnotation: $FlowFixMe, + parser: Parser, +): Nullable { + const unionTypes = parser.remapUnionTypeAnnotationMemberNames( + typeAnnotation.types, + ); + + // Only support unionTypes of the same kind + if (unionTypes.length > 1) { + throw new UnsupportedUnionTypeAnnotationParserError( + hasteModuleName, + typeAnnotation, + unionTypes, + ); + } + + return wrapNullable(nullable, { + type: 'UnionTypeAnnotation', + memberType: unionTypes[0], + }); +} + +function translateArrayTypeAnnotation( + hasteModuleName: string, + types: TypeDeclarationMap, + aliasMap: {...NativeModuleAliasMap}, + enumMap: {...NativeModuleEnumMap}, + cxxOnly: boolean, + arrayType: 'Array' | 'ReadonlyArray', + elementType: $FlowFixMe, + nullable: boolean, + translateTypeAnnotation: $FlowFixMe, + parser: Parser, +): Nullable { + try { + /** + * TODO(T72031674): Migrate all our NativeModule specs to not use + * invalid Array ElementTypes. Then, make the elementType a required + * parameter. + */ + const [_elementType, isElementTypeNullable] = unwrapNullable<$FlowFixMe>( + translateTypeAnnotation( + hasteModuleName, + elementType, + types, + aliasMap, + enumMap, + /** + * TODO(T72031674): Ensure that all ParsingErrors that are thrown + * while parsing the array element don't get captured and collected. + * Why? If we detect any parsing error while parsing the element, + * we should default it to null down the line, here. This is + * the correct behaviour until we migrate all our NativeModule specs + * to be parseable. + */ + nullGuard, + cxxOnly, + parser, + ), + ); + + throwIfArrayElementTypeAnnotationIsUnsupported( + hasteModuleName, + elementType, + arrayType, + _elementType.type, + ); + + return wrapNullable(nullable, { + type: 'ArrayTypeAnnotation', + // $FlowFixMe[incompatible-call] + elementType: wrapNullable(isElementTypeNullable, _elementType), + }); + } catch (ex) { + return wrapNullable(nullable, { + type: 'ArrayTypeAnnotation', + }); + } +} + +function emitArrayType( + hasteModuleName: string, + typeAnnotation: $FlowFixMe, + parser: Parser, + types: TypeDeclarationMap, + aliasMap: {...NativeModuleAliasMap}, + enumMap: {...NativeModuleEnumMap}, + cxxOnly: boolean, + nullable: boolean, + translateTypeAnnotation: $FlowFixMe, +): Nullable { + assertGenericTypeAnnotationHasExactlyOneTypeParameter( + hasteModuleName, + typeAnnotation, + parser, + ); + + return translateArrayTypeAnnotation( + hasteModuleName, + types, + aliasMap, + enumMap, + cxxOnly, + typeAnnotation.type, + typeAnnotation.typeParameters.params[0], + nullable, + translateTypeAnnotation, + parser, + ); +} + +function Visitor(infoMap: {isComponent: boolean, isModule: boolean}): { + [type: string]: (node: $FlowFixMe) => void, +} { + return { + CallExpression(node: $FlowFixMe) { + if ( + node.callee.type === 'Identifier' && + node.callee.name === 'codegenNativeComponent' + ) { + infoMap.isComponent = true; + } + + if (isModuleRegistryCall(node)) { + infoMap.isModule = true; + } + }, + InterfaceExtends(node: $FlowFixMe) { + if (node.id.name === 'TurboModule') { + infoMap.isModule = true; + } + }, + TSInterfaceDeclaration(node: $FlowFixMe) { + if ( + Array.isArray(node.extends) && + node.extends.some( + extension => extension.expression.name === 'TurboModule', + ) + ) { + infoMap.isModule = true; + } + }, + }; +} + +function emitPartial( + nullable: boolean, + hasteModuleName: string, + typeAnnotation: $FlowFixMe, + types: TypeDeclarationMap, + aliasMap: {...NativeModuleAliasMap}, + enumMap: {...NativeModuleEnumMap}, + tryParse: ParserErrorCapturer, + cxxOnly: boolean, + parser: Parser, +): Nullable { + throwIfPartialWithMoreParameter(typeAnnotation); + + throwIfPartialNotAnnotatingTypeParameter(typeAnnotation, types, parser); + + const annotatedElement = parser.extractAnnotatedElement( + typeAnnotation, + types, + ); + const annotatedElementProperties = + parser.getAnnotatedElementProperties(annotatedElement); + + const partialProperties = parser.computePartialProperties( + annotatedElementProperties, + hasteModuleName, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + ); + + return emitObject(nullable, partialProperties); +} + +function emitCommonTypes( + hasteModuleName: string, + types: TypeDeclarationMap, + typeAnnotation: $FlowFixMe, + aliasMap: {...NativeModuleAliasMap}, + enumMap: {...NativeModuleEnumMap}, + tryParse: ParserErrorCapturer, + cxxOnly: boolean, + nullable: boolean, + parser: Parser, +): $FlowFixMe { + const typeMap = { + Stringish: emitStringish, + Int32: emitInt32, + Double: emitDouble, + Float: emitFloat, + UnsafeObject: emitGenericObject, + Object: emitGenericObject, + $Partial: emitPartial, + Partial: emitPartial, + BooleanTypeAnnotation: emitBoolean, + NumberTypeAnnotation: emitNumber, + VoidTypeAnnotation: emitVoid, + StringTypeAnnotation: emitString, + MixedTypeAnnotation: cxxOnly ? emitMixed : emitGenericObject, + }; + + const typeAnnotationName = parser.convertKeywordToTypeAnnotation( + typeAnnotation.type, + ); + + // $FlowFixMe[invalid-computed-prop] + const simpleEmitter = typeMap[typeAnnotationName]; + if (simpleEmitter) { + return simpleEmitter(nullable); + } + + const genericTypeAnnotationName = + parser.getTypeAnnotationName(typeAnnotation); + + // $FlowFixMe[invalid-computed-prop] + const emitter = typeMap[genericTypeAnnotationName]; + if (!emitter) { + return null; + } + + return emitter( + nullable, + hasteModuleName, + typeAnnotation, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + parser, + ); +} + +function emitBoolProp( + name: string, + optional: boolean, +): NamedShape { + return { + name, + optional, + typeAnnotation: { + type: 'BooleanTypeAnnotation', + }, + }; +} + +function emitMixedProp( + name: string, + optional: boolean, +): NamedShape { + return { + name, + optional, + typeAnnotation: { + type: 'MixedTypeAnnotation', + }, + }; +} + +function emitObjectProp( + name: string, + optional: boolean, + parser: Parser, + typeAnnotation: $FlowFixMe, + extractArrayElementType: ( + typeAnnotation: $FlowFixMe, + name: string, + parser: Parser, + ) => EventTypeAnnotation, +): NamedShape { + return { + name, + optional, + typeAnnotation: extractArrayElementType(typeAnnotation, name, parser), + }; +} + +function emitUnionProp( + name: string, + optional: boolean, + parser: Parser, + typeAnnotation: $FlowFixMe, +): NamedShape { + return { + name, + optional, + typeAnnotation: { + type: 'StringEnumTypeAnnotation', + options: typeAnnotation.types.map(option => + parser.getLiteralValue(option), + ), + }, + }; +} + +module.exports = { + emitArrayType, + emitBoolean, + emitBoolProp, + emitDouble, + emitDoubleProp, + emitFloat, + emitFloatProp, + emitFunction, + emitInt32, + emitInt32Prop, + emitMixedProp, + emitNumber, + emitGenericObject, + emitDictionary, + emitObject, + emitPromise, + emitRootTag, + emitVoid, + emitString, + emitStringish, + emitStringProp, + emitMixed, + emitUnion, + emitPartial, + emitCommonTypes, + typeAliasResolution, + typeEnumResolution, + translateArrayTypeAnnotation, + Visitor, + emitObjectProp, + emitUnionProp, +}; diff --git a/packages/react-native-codegen/lib/parsers/parsers-utils.js b/packages/react-native-codegen/lib/parsers/parsers-utils.js new file mode 100644 index 00000000000000..fd91c47e09382b --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/parsers-utils.js @@ -0,0 +1,18 @@ +/** + * 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'; + +function nullGuard(fn) { + return fn(); +} +module.exports = { + nullGuard, +}; diff --git a/packages/react-native-codegen/lib/parsers/parsers-utils.js.flow b/packages/react-native-codegen/lib/parsers/parsers-utils.js.flow new file mode 100644 index 00000000000000..696d308c990d85 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/parsers-utils.js.flow @@ -0,0 +1,19 @@ +/** + * 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 + * @flow strict + */ + +'use strict'; + +function nullGuard(fn: () => T): ?T { + return fn(); +} + +module.exports = { + nullGuard, +}; diff --git a/packages/react-native-codegen/lib/parsers/schema.js b/packages/react-native-codegen/lib/parsers/schema.js new file mode 100644 index 00000000000000..7c9286e2a331e1 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/schema.js @@ -0,0 +1,98 @@ +/** + * 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'; + +function ownKeys(e, r) { + var t = Object.keys(e); + if (Object.getOwnPropertySymbols) { + var o = Object.getOwnPropertySymbols(e); + r && + (o = o.filter(function (r) { + return Object.getOwnPropertyDescriptor(e, r).enumerable; + })), + t.push.apply(t, o); + } + return t; +} +function _objectSpread(e) { + for (var r = 1; r < arguments.length; r++) { + var t = null != arguments[r] ? arguments[r] : {}; + r % 2 + ? ownKeys(Object(t), !0).forEach(function (r) { + _defineProperty(e, r, t[r]); + }) + : Object.getOwnPropertyDescriptors + ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) + : ownKeys(Object(t)).forEach(function (r) { + Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); + }); + } + return e; +} +function _defineProperty(e, r, t) { + return ( + (r = _toPropertyKey(r)) in e + ? Object.defineProperty(e, r, { + value: t, + enumerable: !0, + configurable: !0, + writable: !0, + }) + : (e[r] = t), + e + ); +} +function _toPropertyKey(t) { + var i = _toPrimitive(t, 'string'); + return 'symbol' == typeof i ? i : i + ''; +} +function _toPrimitive(t, r) { + if ('object' != typeof t || !t) return t; + var e = t[Symbol.toPrimitive]; + if (void 0 !== e) { + var i = e.call(t, r || 'default'); + if ('object' != typeof i) return i; + throw new TypeError('@@toPrimitive must return a primitive value.'); + } + return ('string' === r ? String : Number)(t); +} +function wrapComponentSchema({ + filename, + componentName, + extendsProps, + events, + props, + options, + commands, +}) { + return { + modules: { + [filename]: { + type: 'Component', + components: { + [componentName]: _objectSpread( + _objectSpread({}, options || {}), + {}, + { + extendsProps, + events, + props, + commands, + }, + ), + }, + }, + }, + }; +} +module.exports = { + wrapComponentSchema, +}; diff --git a/packages/react-native-codegen/lib/parsers/schema.js.flow b/packages/react-native-codegen/lib/parsers/schema.js.flow new file mode 100644 index 00000000000000..2ebf6158b2dcba --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/schema.js.flow @@ -0,0 +1,62 @@ +/** + * 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 + * @flow strict + */ + +'use strict'; + +import type { + CommandTypeAnnotation, + EventTypeShape, + ExtendsPropsShape, + NamedShape, + OptionsShape, + PropTypeAnnotation, + SchemaType, +} 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, +}; diff --git a/packages/react-native-codegen/lib/parsers/schema/index.d.ts b/packages/react-native-codegen/lib/parsers/schema/index.d.ts new file mode 100644 index 00000000000000..cb298ccc9f15a4 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/schema/index.d.ts @@ -0,0 +1,10 @@ +/** + * 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. + */ + +import type { SchemaType } from '../../CodegenSchema'; + +export declare function parse(filename: string): SchemaType | undefined; diff --git a/packages/react-native-codegen/lib/parsers/schema/index.js b/packages/react-native-codegen/lib/parsers/schema/index.js new file mode 100644 index 00000000000000..6016ece25b6566 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/schema/index.js @@ -0,0 +1,23 @@ +/** + * 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'; + +function parse(filename) { + try { + // $FlowFixMe[unsupported-syntax] Can't require dynamic variables + return require(filename); + } catch (err) { + // Ignore + } +} +module.exports = { + parse, +}; diff --git a/packages/react-native-codegen/lib/parsers/schema/index.js.flow b/packages/react-native-codegen/lib/parsers/schema/index.js.flow new file mode 100644 index 00000000000000..646aa86fcee82d --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/schema/index.js.flow @@ -0,0 +1,26 @@ +/** + * 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-local + * @format + */ + +'use strict'; + +import type {SchemaType} from '../../CodegenSchema.js'; + +function parse(filename: string): ?SchemaType { + try { + // $FlowFixMe[unsupported-syntax] Can't require dynamic variables + return require(filename); + } catch (err) { + // Ignore + } +} + +module.exports = { + parse, +}; diff --git a/packages/react-native-codegen/lib/parsers/typescript/components/__test_fixtures__/failures.js b/packages/react-native-codegen/lib/parsers/typescript/components/__test_fixtures__/failures.js new file mode 100644 index 00000000000000..fe90d864598a40 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/typescript/components/__test_fixtures__/failures.js @@ -0,0 +1,489 @@ +/** + * 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 + */ + +// @licenselint-loose-mode + +'use strict'; + +const COMMANDS_DEFINED_INLINE = ` +/** + * 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 + */ + +const codegenNativeCommands = require('codegenNativeCommands'); +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {Int32} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +export interface ModuleProps extends ViewProps { + // No props +} + +export const Commands = codegenNativeCommands<{ + readonly hotspotUpdate: ( + ref: React.Ref<'RCTView'>, + x: Int32, + y: Int32, + ) => void; +}>({ + supportedCommands: ['hotspotUpdate'], +}); + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; +const COMMANDS_DEFINED_MULTIPLE_TIMES = ` +/** + * 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 + */ + +const codegenNativeCommands = require('codegenNativeCommands'); +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {Int32} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +interface NativeCommands { + readonly hotspotUpdate: ( + viewRef: React.Ref<'RCTView'>, + x: Int32, + y: Int32, + ) => void; +} + +export interface ModuleProps extends ViewProps { + // No props or events +} + +export const Commands = codegenNativeCommands({ + supportedCommands: ['hotspotUpdate'], +}); +export const Commands2 = codegenNativeCommands({ + supportedCommands: ['hotspotUpdate'], +}); + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; +const COMMANDS_DEFINED_WITHOUT_REF = ` +/** + * 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 + */ + +const codegenNativeCommands = require('codegenNativeCommands'); +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {Int32} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +interface NativeCommands { + readonly hotspotUpdate: (x: Int32, y: Int32) => void; +} + +export interface ModuleProps extends ViewProps { + // No props or events +} + +export const Commands = codegenNativeCommands({ + supportedCommands: ['hotspotUpdate'], +}); + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; +const COMMANDS_DEFINED_WITH_NULLABLE_REF = ` +/** + * 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 + */ + +const codegenNativeCommands = require('codegenNativeCommands'); +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {Int32} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +interface NativeCommands { + readonly hotspotUpdate: (viewRef: React.Ref<'RCTView'> | null | undefined, x: Int32, y: Int32) => void; +} + +export interface ModuleProps extends ViewProps { + // No props or events +} + +export const Commands = codegenNativeCommands({ + supportedCommands: ['hotspotUpdate'], +}); + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; +const COMMANDS_DEFINED_WITH_MISMATCHED_METHOD_NAMES = ` +/** + * 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 + */ + +const codegenNativeCommands = require('codegenNativeCommands'); +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {Int32} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +interface NativeCommands { + readonly hotspotUpdate: (viewRef: React.Ref<'RCTView'>, x: Int32, y: Int32) => void; + readonly scrollTo: ( + viewRef: React.Ref<'RCTView'>, + y: Int32, + animated: boolean, + ) => void; +} + +export interface ModuleProps extends ViewProps { + // No props or events +} + +export const Commands = codegenNativeCommands({ + supportedCommands: ['scrollTo'], +}); +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; +const COMMANDS_DEFINED_WITHOUT_METHOD_NAMES = ` +/** + * 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 + */ + +const codegenNativeCommands = require('codegenNativeCommands'); +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {Int32} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +interface NativeCommands { + readonly hotspotUpdate: (viewRef: React.Ref<'RCTView'>, x: Int32, y: Int32) => void; + readonly scrollTo: ( + viewRef: React.Ref<'RCTView'>, + y: Int32, + animated: boolean, + ) => void; +} + +export interface ModuleProps extends ViewProps { + // No props or events +} + +export const Commands = codegenNativeCommands(); + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; +const NULLABLE_WITH_DEFAULT = ` +/** + * 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 + */ + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {WithDefault, Float} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +export interface ModuleProps extends ViewProps { + nullable_with_default: WithDefault | null | undefined; +} + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; +const NON_OPTIONAL_KEY_WITH_DEFAULT_VALUE = ` +/** + * 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 + */ + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {WithDefault, Float} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +export interface ModuleProps extends ViewProps { + required_key_with_default: WithDefault; +} + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; +const PROPS_CONFLICT_NAMES = ` +/** + * 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 + */ + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +export interface ModuleProps extends ViewProps { + isEnabled: string, + + isEnabled: boolean, +} + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; +const PROPS_CONFLICT_WITH_SPREAD_PROPS = ` +/** + * 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 + */ + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +type PropsInFile = Readonly<{ + isEnabled: boolean, +}>; + +export interface ModuleProps extends ViewProps, PropsInFile { + isEnabled: boolean, +} + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; +const PROP_NUMBER_TYPE = ` +/** + * 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 + */ + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +export interface ModuleProps extends ViewProps { + someProp: number +} + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; +const PROP_MIXED_ENUM = ` +/** + * 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 + */ + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; +import type {WithDefault} from 'CodegenTypes'; + +export interface ModuleProps extends ViewProps { + someProp?: WithDefault<'foo' | 1, 1>; +} + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; +const PROP_ENUM_BOOLEAN = ` +/** + * 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 + */ + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; +import type {WithDefault} from 'CodegenTypes'; + +export interface ModuleProps extends ViewProps { + someProp?: WithDefault +} + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; +const PROP_ARRAY_MIXED_ENUM = ` +/** + * 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 + */ + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; +import type {WithDefault} from 'CodegenTypes'; + +export interface ModuleProps extends ViewProps { + someProp?: WithDefault, 1>; +} + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; +const PROP_ARRAY_ENUM_BOOLEAN = ` +/** + * 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 + */ + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; +import type {WithDefault} from 'CodegenTypes'; + +export interface ModuleProps extends ViewProps { + someProp?: WithDefault, false>; +} + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; +const PROP_ARRAY_ENUM_INT = ` +/** + * 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 + */ + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; +import type {WithDefault} from 'CodegenTypes'; + +export interface ModuleProps extends ViewProps { + someProp?: WithDefault, 0>; +} + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; +module.exports = { + COMMANDS_DEFINED_INLINE, + COMMANDS_DEFINED_MULTIPLE_TIMES, + COMMANDS_DEFINED_WITH_MISMATCHED_METHOD_NAMES, + COMMANDS_DEFINED_WITHOUT_METHOD_NAMES, + COMMANDS_DEFINED_WITHOUT_REF, + COMMANDS_DEFINED_WITH_NULLABLE_REF, + NULLABLE_WITH_DEFAULT, + NON_OPTIONAL_KEY_WITH_DEFAULT_VALUE, + PROPS_CONFLICT_NAMES, + PROPS_CONFLICT_WITH_SPREAD_PROPS, + PROP_NUMBER_TYPE, + PROP_MIXED_ENUM, + PROP_ENUM_BOOLEAN, + PROP_ARRAY_MIXED_ENUM, + PROP_ARRAY_ENUM_BOOLEAN, + PROP_ARRAY_ENUM_INT, +}; diff --git a/packages/react-native-codegen/lib/parsers/typescript/components/__test_fixtures__/failures.js.flow b/packages/react-native-codegen/lib/parsers/typescript/components/__test_fixtures__/failures.js.flow new file mode 100644 index 00000000000000..eefd9e316f18e5 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/typescript/components/__test_fixtures__/failures.js.flow @@ -0,0 +1,505 @@ +/** + * 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-local + * @format + */ + +// @licenselint-loose-mode + +'use strict'; + +const COMMANDS_DEFINED_INLINE = ` +/** + * 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 + */ + +const codegenNativeCommands = require('codegenNativeCommands'); +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {Int32} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +export interface ModuleProps extends ViewProps { + // No props +} + +export const Commands = codegenNativeCommands<{ + readonly hotspotUpdate: ( + ref: React.Ref<'RCTView'>, + x: Int32, + y: Int32, + ) => void; +}>({ + supportedCommands: ['hotspotUpdate'], +}); + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; + +const COMMANDS_DEFINED_MULTIPLE_TIMES = ` +/** + * 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 + */ + +const codegenNativeCommands = require('codegenNativeCommands'); +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {Int32} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +interface NativeCommands { + readonly hotspotUpdate: ( + viewRef: React.Ref<'RCTView'>, + x: Int32, + y: Int32, + ) => void; +} + +export interface ModuleProps extends ViewProps { + // No props or events +} + +export const Commands = codegenNativeCommands({ + supportedCommands: ['hotspotUpdate'], +}); +export const Commands2 = codegenNativeCommands({ + supportedCommands: ['hotspotUpdate'], +}); + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; + +const COMMANDS_DEFINED_WITHOUT_REF = ` +/** + * 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 + */ + +const codegenNativeCommands = require('codegenNativeCommands'); +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {Int32} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +interface NativeCommands { + readonly hotspotUpdate: (x: Int32, y: Int32) => void; +} + +export interface ModuleProps extends ViewProps { + // No props or events +} + +export const Commands = codegenNativeCommands({ + supportedCommands: ['hotspotUpdate'], +}); + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; + +const COMMANDS_DEFINED_WITH_NULLABLE_REF = ` +/** + * 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 + */ + +const codegenNativeCommands = require('codegenNativeCommands'); +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {Int32} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +interface NativeCommands { + readonly hotspotUpdate: (viewRef: React.Ref<'RCTView'> | null | undefined, x: Int32, y: Int32) => void; +} + +export interface ModuleProps extends ViewProps { + // No props or events +} + +export const Commands = codegenNativeCommands({ + supportedCommands: ['hotspotUpdate'], +}); + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; + +const COMMANDS_DEFINED_WITH_MISMATCHED_METHOD_NAMES = ` +/** + * 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 + */ + +const codegenNativeCommands = require('codegenNativeCommands'); +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {Int32} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +interface NativeCommands { + readonly hotspotUpdate: (viewRef: React.Ref<'RCTView'>, x: Int32, y: Int32) => void; + readonly scrollTo: ( + viewRef: React.Ref<'RCTView'>, + y: Int32, + animated: boolean, + ) => void; +} + +export interface ModuleProps extends ViewProps { + // No props or events +} + +export const Commands = codegenNativeCommands({ + supportedCommands: ['scrollTo'], +}); +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; + +const COMMANDS_DEFINED_WITHOUT_METHOD_NAMES = ` +/** + * 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 + */ + +const codegenNativeCommands = require('codegenNativeCommands'); +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {Int32} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +interface NativeCommands { + readonly hotspotUpdate: (viewRef: React.Ref<'RCTView'>, x: Int32, y: Int32) => void; + readonly scrollTo: ( + viewRef: React.Ref<'RCTView'>, + y: Int32, + animated: boolean, + ) => void; +} + +export interface ModuleProps extends ViewProps { + // No props or events +} + +export const Commands = codegenNativeCommands(); + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; + +const NULLABLE_WITH_DEFAULT = ` +/** + * 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 + */ + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {WithDefault, Float} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +export interface ModuleProps extends ViewProps { + nullable_with_default: WithDefault | null | undefined; +} + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; + +const NON_OPTIONAL_KEY_WITH_DEFAULT_VALUE = ` +/** + * 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 + */ + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {WithDefault, Float} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +export interface ModuleProps extends ViewProps { + required_key_with_default: WithDefault; +} + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; + +const PROPS_CONFLICT_NAMES = ` +/** + * 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 + */ + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +export interface ModuleProps extends ViewProps { + isEnabled: string, + + isEnabled: boolean, +} + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; + +const PROPS_CONFLICT_WITH_SPREAD_PROPS = ` +/** + * 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 + */ + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +type PropsInFile = Readonly<{ + isEnabled: boolean, +}>; + +export interface ModuleProps extends ViewProps, PropsInFile { + isEnabled: boolean, +} + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; + +const PROP_NUMBER_TYPE = ` +/** + * 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 + */ + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +export interface ModuleProps extends ViewProps { + someProp: number +} + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; + +const PROP_MIXED_ENUM = ` +/** + * 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 + */ + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; +import type {WithDefault} from 'CodegenTypes'; + +export interface ModuleProps extends ViewProps { + someProp?: WithDefault<'foo' | 1, 1>; +} + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; + +const PROP_ENUM_BOOLEAN = ` +/** + * 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 + */ + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; +import type {WithDefault} from 'CodegenTypes'; + +export interface ModuleProps extends ViewProps { + someProp?: WithDefault +} + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; + +const PROP_ARRAY_MIXED_ENUM = ` +/** + * 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 + */ + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; +import type {WithDefault} from 'CodegenTypes'; + +export interface ModuleProps extends ViewProps { + someProp?: WithDefault, 1>; +} + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; + +const PROP_ARRAY_ENUM_BOOLEAN = ` +/** + * 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 + */ + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; +import type {WithDefault} from 'CodegenTypes'; + +export interface ModuleProps extends ViewProps { + someProp?: WithDefault, false>; +} + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; + +const PROP_ARRAY_ENUM_INT = ` +/** + * 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 + */ + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; +import type {WithDefault} from 'CodegenTypes'; + +export interface ModuleProps extends ViewProps { + someProp?: WithDefault, 0>; +} + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; + +module.exports = { + COMMANDS_DEFINED_INLINE, + COMMANDS_DEFINED_MULTIPLE_TIMES, + COMMANDS_DEFINED_WITH_MISMATCHED_METHOD_NAMES, + COMMANDS_DEFINED_WITHOUT_METHOD_NAMES, + COMMANDS_DEFINED_WITHOUT_REF, + COMMANDS_DEFINED_WITH_NULLABLE_REF, + NULLABLE_WITH_DEFAULT, + NON_OPTIONAL_KEY_WITH_DEFAULT_VALUE, + PROPS_CONFLICT_NAMES, + PROPS_CONFLICT_WITH_SPREAD_PROPS, + PROP_NUMBER_TYPE, + PROP_MIXED_ENUM, + PROP_ENUM_BOOLEAN, + PROP_ARRAY_MIXED_ENUM, + PROP_ARRAY_ENUM_BOOLEAN, + PROP_ARRAY_ENUM_INT, +}; diff --git a/packages/react-native-codegen/lib/parsers/typescript/components/__test_fixtures__/fixtures.js b/packages/react-native-codegen/lib/parsers/typescript/components/__test_fixtures__/fixtures.js new file mode 100644 index 00000000000000..042b6a58227aaf --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/typescript/components/__test_fixtures__/fixtures.js @@ -0,0 +1,1235 @@ +/** + * 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 + */ + +// @licenselint-loose-mode + +'use strict'; + +const EVENT_DEFINITION = ` + boolean_required: boolean; + boolean_optional_key?: boolean; + boolean_optional_value: boolean | null | undefined; + boolean_optional_both?: boolean | null | undefined; + + string_required: string; + string_optional_key?: (string); + string_optional_value: (string) | null | undefined; + string_optional_both?: (string | null | undefined); + + double_required: Double; + double_optional_key?: Double; + double_optional_value: Double | null | undefined; + double_optional_both?: Double | null | undefined; + + float_required: Float; + float_optional_key?: Float; + float_optional_value: Float | null | undefined; + float_optional_both?: Float | null | undefined; + + int32_required: Int32; + int32_optional_key?: Int32; + int32_optional_value: Int32 | null | undefined; + int32_optional_both?: Int32 | null | undefined; + + enum_required: 'small' | 'large'; + enum_optional_key?: 'small' | 'large'; + enum_optional_value: ('small' | 'large') | null | undefined; + enum_optional_both?: ('small' | 'large') | null | undefined; + + object_required: { + boolean_required: boolean; + }; + + object_optional_key?: { + string_optional_key?: string; + }; + + object_optional_value: { + float_optional_value: Float | null | undefined; + } | null | undefined; + + object_optional_both?: { + int32_optional_both?: Int32 | null | undefined; + } | null | undefined; + + object_required_nested_2_layers: { + object_optional_nested_1_layer?: { + boolean_required: Int32; + string_optional_key?: string; + double_optional_value: Double | null | undefined; + float_optional_value: Float | null | undefined; + int32_optional_both?: Int32 | null | undefined; + } | null | undefined; + }; + + object_readonly_required: Readonly<{ + boolean_required: boolean; + }>; + + object_readonly_optional_key?: Readonly<{ + string_optional_key?: string; + }>; + + object_readonly_optional_value: Readonly<{ + float_optional_value: Float | null | undefined; + }> | null | undefined; + + object_readonly_optional_both?: Readonly<{ + int32_optional_both?: Int32 | null | undefined; + }> | null | undefined; + + boolean_array_required: boolean[]; + boolean_array_optional_key?: boolean[]; + boolean_array_optional_value: boolean[] | null | undefined; + boolean_array_optional_both?: boolean[] | null | undefined; + + string_array_required: string[]; + string_array_optional_key?: (string[]); + string_array_optional_value: (string[]) | null | undefined; + string_array_optional_both?: (string[] | null | undefined); + + double_array_required: Double[]; + double_array_optional_key?: Double[]; + double_array_optional_value: Double[] | null | undefined; + double_array_optional_both?: Double[] | null | undefined; + + float_array_required: Float[]; + float_array_optional_key?: Float[]; + float_array_optional_value: Float[] | null | undefined; + float_array_optional_both?: Float[] | null | undefined; + + int32_array_required: Int32[]; + int32_array_optional_key?: Int32[]; + int32_array_optional_value: Int32[] | null | undefined; + int32_array_optional_both?: Int32[] | null | undefined; + + enum_array_required: ('small' | 'large')[]; + enum_array_optional_key?: ('small' | 'large')[]; + enum_array_optional_value: ('small' | 'large')[] | null | undefined; + enum_array_optional_both?: ('small' | 'large')[] | null | undefined; + + object_array_required: { + boolean_required: boolean; + }[]; + + object_array_optional_key?: { + string_optional_key?: string; + }[]; + + object_array_optional_value: { + float_optional_value: Float | null | undefined; + }[] | null | undefined; + + object_array_optional_both?: { + int32_optional_both?: Int32 | null | undefined; + }[] | null | undefined; + + int32_array_array_required: Int32[][]; + int32_array_array_optional_key?: Int32[][]; + int32_array_array_optional_value: Int32[][] | null | undefined; + int32_array_array_optional_both?: Int32[][] | null | undefined; +`; +const ONE_OF_EACH_PROP_EVENT_DEFAULT_AND_OPTIONS = ` +/** + * 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'; + +import type { + BubblingEventHandler, + DirectEventHandler, + WithDefault, +} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +export interface ModuleProps extends ViewProps { + // Props + boolean_default_true_optional_both?: WithDefault; + + // Events + onDirectEventDefinedInlineNull: DirectEventHandler; + onBubblingEventDefinedInlineNull: BubblingEventHandler; +} + +export default codegenNativeComponent('Module', { + interfaceOnly: true, + paperComponentName: 'RCTModule', +}) as HostComponent; +`; +const ONE_OF_EACH_PROP_EVENT_DEFAULT_AND_OPTIONS_NO_CAST = ` +/** + * 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 + */ + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type { + BubblingEventHandler, + DirectEventHandler, + WithDefault, +} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +export interface ModuleProps extends ViewProps { + // Props + boolean_default_true_optional_both?: WithDefault; + + // Events + onDirectEventDefinedInlineNull: DirectEventHandler; + onBubblingEventDefinedInlineNull: BubblingEventHandler; +} + +export default codegenNativeComponent('Module', { + interfaceOnly: true, + excludedPlatforms: ['android'], + paperComponentName: 'RCTModule', +}) as HostComponent; +`; +const NO_PROPS_EVENTS_ONLY_DEPRECATED_VIEW_CONFIG_NAME_OPTION = ` +/** + * 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 + */ + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +export interface ModuleProps extends ViewProps { + +} + +export default codegenNativeComponent('Module', { + deprecatedViewConfigName: 'DeprecateModuleName', +}) as HostComponent; +`; +const ALL_PROP_TYPES_NO_EVENTS = ` +/** + * 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 codegenNativeComponent = require('codegenNativeComponent'); + +import type {Int32, Double, Float, WithDefault, UnsafeMixed} from 'CodegenTypes'; +import type {ImageSource} from 'ImageSource'; +import type { + ColorValue, + ColorArrayValue, + PointValue, + EdgeInsetsValue, + DimensionValue, +} from 'StyleSheetTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +export interface ModuleProps extends ViewProps { + // Props + // Boolean props + boolean_required: boolean; + boolean_optional_key?: WithDefault; + boolean_optional_both?: WithDefault; + + // Boolean props, null default + boolean_null_optional_key?: WithDefault; + boolean_null_optional_both?: WithDefault; + + // String props + string_required: string; + string_optional_key?: WithDefault; + string_optional_both?: WithDefault; + + // String props, null default + string_null_optional_key?: WithDefault; + string_null_optional_both?: WithDefault; + + // Stringish props + stringish_required: Stringish; + stringish_optional_key?: WithDefault; + stringish_optional_both?: WithDefault; + + // Stringish props, null default + stringish_null_optional_key?: WithDefault; + stringish_null_optional_both?: WithDefault; + + // Double props + double_required: Double; + double_optional_key?: WithDefault; + double_optional_both?: WithDefault; + + // Float props + float_required: Float; + float_optional_key?: WithDefault; + float_optional_both?: WithDefault; + + // Float props, null default + float_null_optional_key?: WithDefault; + float_null_optional_both?: WithDefault; + + // Int32 props + int32_required: Int32; + int32_optional_key?: WithDefault; + int32_optional_both?: WithDefault; + + // String enum props + enum_optional_key?: WithDefault<'small' | 'large', 'small'>; + enum_optional_both?: WithDefault<'small' | 'large', 'small'>; + + // Int enum props + int_enum_optional_key?: WithDefault<0 | 1, 0>; + + // Object props + object_optional_key?: Readonly<{prop: string}>; + object_optional_both?: Readonly<{prop: string} | null | undefined>; + object_optional_value: Readonly<{prop: string} | null | undefined>; + + // ImageSource props + image_required: ImageSource; + image_optional_value: ImageSource | null | undefined; + image_optional_both?: ImageSource | null | undefined; + + // ColorValue props + color_required: ColorValue; + color_optional_key?: ColorValue; + color_optional_value: ColorValue | null | undefined; + color_optional_both?: ColorValue | null | undefined; + + // ColorArrayValue props + color_array_required: ColorArrayValue; + color_array_optional_key?: ColorArrayValue; + color_array_optional_value: ColorArrayValue | null | undefined; + color_array_optional_both?: ColorArrayValue | null | undefined; + + // ProcessedColorValue props + processed_color_required: ProcessedColorValue; + processed_color_optional_key?: ProcessedColorValue; + processed_color_optional_value: ProcessedColorValue | null | undefined; + processed_color_optional_both?: ProcessedColorValue | null | undefined; + + // PointValue props + point_required: PointValue; + point_optional_key?: PointValue; + point_optional_value: PointValue | null | undefined; + point_optional_both?: PointValue | null | undefined; + + // EdgeInsets props + insets_required: EdgeInsetsValue; + insets_optional_key?: EdgeInsetsValue; + insets_optional_value: EdgeInsetsValue | null | undefined; + insets_optional_both?: EdgeInsetsValue | null | undefined; + + + // DimensionValue props + dimension_required: DimensionValue; + dimension_optional_key?: DimensionValue; + dimension_optional_value: DimensionValue | null | undefined; + dimension_optional_both?: DimensionValue | null | undefined; + + // Mixed props + mixed_required: UnsafeMixed, + mixed_optional_key?: UnsafeMixed, +} + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; +const ARRAY_PROP_TYPES_NO_EVENTS = ` +/** + * 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 codegenNativeComponent = require('codegenNativeComponent'); + +import type {Int32, Double, Float, WithDefault} from 'CodegenTypes'; +import type {ImageSource} from 'ImageSource'; +import type { + ColorValue, + ColorArrayValue, + PointValue, + EdgeInsetsValue, + DimensionValue, +} from 'StyleSheetTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +type ObjectType = Readonly<{prop: string}>; +type ArrayObjectType = ReadonlyArray>; + +export interface ModuleProps extends ViewProps { + // Props + // Boolean props + array_boolean_required: ReadonlyArray; + array_boolean_optional_key?: ReadonlyArray; + array_boolean_optional_value: ReadonlyArray | null | undefined; + array_boolean_optional_both?: ReadonlyArray | null | undefined; + + // String props + array_string_required: ReadonlyArray; + array_string_optional_key?: ReadonlyArray; + array_string_optional_value: ReadonlyArray | null | undefined; + array_string_optional_both?: ReadonlyArray | null | undefined; + + // Double props + array_double_required: ReadonlyArray; + array_double_optional_key?: ReadonlyArray; + array_double_optional_value: ReadonlyArray | null | undefined; + array_double_optional_both?: ReadonlyArray | null | undefined; + + // Float props + array_float_required: ReadonlyArray; + array_float_optional_key?: ReadonlyArray; + array_float_optional_value: ReadonlyArray | null | undefined; + array_float_optional_both?: ReadonlyArray | null | undefined; + + // Int32 props + array_int32_required: ReadonlyArray; + array_int32_optional_key?: ReadonlyArray; + array_int32_optional_value: ReadonlyArray | null | undefined; + array_int32_optional_both?: ReadonlyArray | null | undefined; + + // String enum props + array_enum_optional_key?: WithDefault< + ReadonlyArray<'small' | 'large'>, + 'small' + >; + array_enum_optional_both?: WithDefault< + ReadonlyArray<'small' | 'large'>, + 'small' + >; + + // ImageSource props + array_image_required: ReadonlyArray; + array_image_optional_key?: ReadonlyArray; + array_image_optional_value: ReadonlyArray | null | undefined; + array_image_optional_both?: ReadonlyArray | null | undefined; + + // ColorValue props + array_color_required: ReadonlyArray; + array_color_optional_key?: ReadonlyArray; + array_color_optional_value: ReadonlyArray | null | undefined; + array_color_optional_both?: ReadonlyArray | null | undefined; + + // PointValue props + array_point_required: ReadonlyArray; + array_point_optional_key?: ReadonlyArray; + array_point_optional_value: ReadonlyArray | null | undefined; + array_point_optional_both?: ReadonlyArray | null | undefined; + + // EdgeInsetsValue props + array_insets_required: ReadonlyArray; + array_insets_optional_key?: ReadonlyArray; + array_insets_optional_value: ReadonlyArray | null | undefined; + array_insets_optional_both?: ReadonlyArray | null | undefined; + + // DimensionValue props + array_dimension_required: ReadonlyArray; + array_dimension_optional_key?: ReadonlyArray; + array_dimension_optional_value: ReadonlyArray | null | undefined; + array_dimension_optional_both?: ReadonlyArray | null | undefined; + + // Object props + array_object_required: ReadonlyArray>; + array_object_optional_key?: ReadonlyArray>; + array_object_optional_value: ArrayObjectType | null | undefined; + array_object_optional_both?: ReadonlyArray | null | undefined; + + // Nested array object types + array_of_array_object_required: ReadonlyArray< + Readonly<{ + // This needs to be the same name as the top level array above + array_object_required: ReadonlyArray>; + }> + >; + array_of_array_object_optional_key?: ReadonlyArray< + Readonly<{ + // This needs to be the same name as the top level array above + array_object_optional_key: ReadonlyArray>; + }> + >; + array_of_array_object_optional_value: ReadonlyArray< + Readonly<{ + // This needs to be the same name as the top level array above + array_object_optional_value: ReadonlyArray< + Readonly<{prop: string | null | undefined}> + >; + }> + > | null | undefined; + array_of_array_object_optional_both?: ReadonlyArray< + Readonly<{ + // This needs to be the same name as the top level array above + array_object_optional_both: ReadonlyArray< + Readonly<{prop?: string | null | undefined}> + >; + }> + > | null | undefined; + + // Nested array of array of object types + array_of_array_of_object_required: ReadonlyArray< + ReadonlyArray< + Readonly<{ + prop: string; + }> + > + >; + + // Nested array of array of object types (in file) + array_of_array_of_object_required_in_file: ReadonlyArray< + ReadonlyArray + >; + + // Nested array of array of object types (with spread) + array_of_array_of_object_required_with_spread: ReadonlyArray< + ReadonlyArray< + Readonly, + >, + >, +} + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; +const ARRAY2_PROP_TYPES_NO_EVENTS = ` +/** + * 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 codegenNativeComponent = require('codegenNativeComponent'); + +import type {Int32, Double, Float, WithDefault} from 'CodegenTypes'; +import type {ImageSource} from 'ImageSource'; +import type { + ColorValue, + ColorArrayValue, + PointValue, + EdgeInsetsValue, +} from 'StyleSheetTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +type ObjectType = Readonly<{prop: string}>; +type ArrayObjectType = readonly Readonly<{prop: string}>[]; + +export interface ModuleProps extends ViewProps { + // Props + // Boolean props + array_boolean_required: readonly boolean[]; + array_boolean_optional_key?: readonly boolean[]; + array_boolean_optional_value: readonly boolean[] | null | undefined; + array_boolean_optional_both?: readonly boolean[] | null | undefined; + + // String props + array_string_required: readonly string[]; + array_string_optional_key?: readonly string[]; + array_string_optional_value: readonly string[] | null | undefined; + array_string_optional_both?: readonly string[] | null | undefined; + + // Double props + array_double_required: readonly Double[]; + array_double_optional_key?: readonly Double[]; + array_double_optional_value: readonly Double[] | null | undefined; + array_double_optional_both?: readonly Double[] | null | undefined; + + // Float props + array_float_required: readonly Float[]; + array_float_optional_key?: readonly Float[]; + array_float_optional_value: readonly Float[] | null | undefined; + array_float_optional_both?: readonly Float[] | null | undefined; + + // Int32 props + array_int32_required: readonly Int32[]; + array_int32_optional_key?: readonly Int32[]; + array_int32_optional_value: readonly Int32[] | null | undefined; + array_int32_optional_both?: readonly Int32[] | null | undefined; + + // String enum props + array_enum_optional_key?: WithDefault< + readonly ('small' | 'large')[], + 'small' + >; + array_enum_optional_both?: WithDefault< + readonly ('small' | 'large')[], + 'small' + >; + + // ImageSource props + array_image_required: readonly ImageSource[]; + array_image_optional_key?: readonly ImageSource[]; + array_image_optional_value: readonly ImageSource[] | null | undefined; + array_image_optional_both?: readonly ImageSource[] | null | undefined; + + // ColorValue props + array_color_required: readonly ColorValue[]; + array_color_optional_key?: readonly ColorValue[]; + array_color_optional_value: readonly ColorValue[] | null | undefined; + array_color_optional_both?: readonly ColorValue[] | null | undefined; + + // PointValue props + array_point_required: readonly PointValue[]; + array_point_optional_key?: readonly PointValue[]; + array_point_optional_value: readonly PointValue[] | null | undefined; + array_point_optional_both?: readonly PointValue[] | null | undefined; + + // EdgeInsetsValue props + array_insets_required: readonly EdgeInsetsValue[]; + array_insets_optional_key?: readonly EdgeInsetsValue[]; + array_insets_optional_value: readonly EdgeInsetsValue[] | null | undefined; + array_insets_optional_both?: readonly EdgeInsetsValue[] | null | undefined; + + // Object props + array_object_required: readonly Readonly<{prop: string}>[]; + array_object_optional_key?: readonly Readonly<{prop: string}>[]; + array_object_optional_value: ArrayObjectType | null | undefined; + array_object_optional_both?: readonly ObjectType[] | null | undefined; + + // Nested array object types + array_of_array_object_required: readonly Readonly<{ + // This needs to be the same name as the top level array above + array_object_required: readonly Readonly<{prop: string}>[]; + }>[]; + array_of_array_object_optional_key?: readonly Readonly<{ + // This needs to be the same name as the top level array above + array_object_optional_key: readonly Readonly<{prop?: string}>[]; + }>[]; + array_of_array_object_optional_value: readonly Readonly<{ + // This needs to be the same name as the top level array above + array_object_optional_value: readonly Readonly<{prop: string | null | undefined}>[]; + }>[] | null | undefined; + array_of_array_object_optional_both?: readonly Readonly<{ + // This needs to be the same name as the top level array above + array_object_optional_both: readonly Readonly<{prop?: string | null | undefined}>[]; + }>[] | null | undefined; + + // Nested array of array of object types + array_of_array_of_object_required: readonly Readonly<{ + prop: string; + }>[][]; + + // Nested array of array of object types (in file) + array_of_array_of_object_required_in_file: readonly ObjectType[][]; + + // Nested array of array of object types (with spread) + array_of_array_of_object_required_with_spread: readonly Readonly[][]; +} + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; +const OBJECT_PROP_TYPES_NO_EVENTS = ` +/** + * 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 codegenNativeComponent = require('codegenNativeComponent'); + +import type {Int32, Double, Float, WithDefault} from 'CodegenTypes'; +import type {ImageSource} from 'ImageSource'; +import type { + ColorValue, + ColorArrayValue, + PointValue, + EdgeInsetsValue, + DimensionValue, +} from 'StyleSheetTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +export interface ModuleProps extends ViewProps { + // Props + // Boolean props + boolean_required: Readonly<{prop: boolean}>; + boolean_optional: Readonly<{prop?: WithDefault}>; + + // String props + string_required: Readonly<{prop: string}>; + string_optional: Readonly<{prop?: WithDefault}>; + + // Double props + double_required: Readonly<{prop: Double}>; + double_optional: Readonly<{prop?: WithDefault}>; + + // Float props + float_required: Readonly<{prop: Float}>; + float_optional: Readonly<{prop?: WithDefault}>; + + // Int32 props + int_required: Readonly<{prop: Int32}>; + int_optional: Readonly<{prop?: WithDefault}>; + + // String enum props + enum_optional: Readonly<{ + prop?: WithDefault, 'small'>; + }>; + + // ImageSource props + image_required: Readonly<{prop: ImageSource}>; + image_optional_key: Readonly<{prop?: ImageSource}>; + image_optional_value: Readonly<{prop: ImageSource | null | undefined}>; + image_optional_both: Readonly<{prop?: ImageSource | null | undefined}>; + + // ColorValue props + color_required: Readonly<{prop: ColorValue}>; + color_optional_key: Readonly<{prop?: ColorValue}>; + color_optional_value: Readonly<{prop: ColorValue | null | undefined}>; + color_optional_both: Readonly<{prop?: ColorValue | null | undefined}>; + + // ProcessedColorValue props + processed_color_required: Readonly<{prop: ProcessedColorValue}>; + processed_color_optional_key: Readonly<{prop?: ProcessedColorValue}>; + processed_color_optional_value: Readonly<{ + prop: ProcessedColorValue | null | undefined; + }>; + processed_color_optional_both: Readonly<{ + prop?: ProcessedColorValue | null | undefined; + }>; + + // PointValue props + point_required: Readonly<{prop: PointValue}>; + point_optional_key: Readonly<{prop?: PointValue}>; + point_optional_value: Readonly<{prop: PointValue | null | undefined}>; + point_optional_both: Readonly<{prop?: PointValue | null | undefined}>; + + // EdgeInsetsValue props + insets_required: Readonly<{prop: EdgeInsetsValue}>; + insets_optional_key: Readonly<{prop?: EdgeInsetsValue}>; + insets_optional_value: Readonly<{prop: EdgeInsetsValue | null | undefined}>; + insets_optional_both: Readonly<{prop?: EdgeInsetsValue | null | undefined}>; + + // DimensionValue props + dimension_required: Readonly<{prop: DimensionValue}>; + dimension_optional_key: Readonly<{prop?: DimensionValue}>; + dimension_optional_value: Readonly<{prop: DimensionValue | null | undefined}>; + dimension_optional_both: Readonly<{prop?: DimensionValue | null | undefined}>; + + // Nested object props + object_required: Readonly<{prop: Readonly<{nestedProp: string}>}>; + object_optional_key?: Readonly<{prop: Readonly<{nestedProp: string}>}>; + object_optional_value: Readonly<{ + prop: Readonly<{nestedProp: string}>; + }> | null | undefined; + object_optional_both?: Readonly<{ + prop: Readonly<{nestedProp: string}>; + }> | null | undefined; +} + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; +const PROPS_ALIASED_LOCALLY = ` +/** + * 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'; + +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +type DeepSpread = Readonly<{ + otherStringProp: string; +}>; + +export interface PropsInFile extends DeepSpread { + isEnabled: boolean; + label: string; +} + +type ReadOnlyPropsInFile = Readonly; + +export interface ModuleProps extends ViewProps, ReadOnlyPropsInFile { + localType: ReadOnlyPropsInFile; + localArr: ReadonlyArray; +} + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; +const EVENTS_DEFINED_INLINE_WITH_ALL_TYPES = ` +/** + * 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 + */ + +import type {HostComponent} from 'react-native'; +const codegenNativeComponent = require('codegenNativeComponent'); + +import type { + Int32, + Double, + Float, + BubblingEventHandler, + DirectEventHandler, +} from 'CodegenTypes'; + +import type {ViewProps} from 'ViewPropTypes'; + +export interface ModuleProps extends ViewProps { + // No Props + + // Events + onDirectEventDefinedInline: DirectEventHandler< + Readonly<{ + ${EVENT_DEFINITION} + }> + >; + + onDirectEventDefinedInlineOptionalKey?: (DirectEventHandler< + Readonly<{ + ${EVENT_DEFINITION} + }> + >); + + onDirectEventDefinedInlineOptionalValue: (DirectEventHandler< + Readonly<{ + ${EVENT_DEFINITION} + }> + >) | null | undefined; + + onDirectEventDefinedInlineOptionalBoth?: (DirectEventHandler< + Readonly<{ + ${EVENT_DEFINITION} + }> + > | null | undefined); + + onDirectEventDefinedInlineWithPaperName?: DirectEventHandler< + Readonly<{ + ${EVENT_DEFINITION} + }>, + 'paperDirectEventDefinedInlineWithPaperName' + > | null | undefined; + + onBubblingEventDefinedInline: BubblingEventHandler< + Readonly<{ + ${EVENT_DEFINITION} + }> + >; + + onBubblingEventDefinedInlineOptionalKey?: BubblingEventHandler< + Readonly<{ + ${EVENT_DEFINITION} + }> + >; + + onBubblingEventDefinedInlineOptionalValue: BubblingEventHandler< + Readonly<{ + ${EVENT_DEFINITION} + }> + > | null | undefined; + + onBubblingEventDefinedInlineOptionalBoth?: BubblingEventHandler< + Readonly<{ + ${EVENT_DEFINITION} + }> + > | null | undefined; + + onBubblingEventDefinedInlineWithPaperName?: BubblingEventHandler< + Readonly<{ + ${EVENT_DEFINITION} + }>, + 'paperBubblingEventDefinedInlineWithPaperName' + > | null | undefined; +} + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; +const EVENTS_DEFINED_AS_NULL_INLINE = ` +/** + * 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 codegenNativeComponent = require('codegenNativeComponent'); + +import type {BubblingEventHandler, DirectEventHandler} from 'CodegenTypese'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +export interface ModuleProps extends ViewProps { + // No props + + // Events defined inline + onDirectEventDefinedInlineNull: DirectEventHandler; + onDirectEventDefinedInlineNullOptionalKey?: DirectEventHandler; + onDirectEventDefinedInlineNullOptionalValue: DirectEventHandler | null | undefined; + onDirectEventDefinedInlineNullOptionalBoth?: DirectEventHandler; + onDirectEventDefinedInlineNullWithPaperName?: DirectEventHandler< + null, + 'paperDirectEventDefinedInlineNullWithPaperName' + > | null | undefined; + + onBubblingEventDefinedInlineNull: BubblingEventHandler; + onBubblingEventDefinedInlineNullOptionalKey?: BubblingEventHandler; + onBubblingEventDefinedInlineNullOptionalValue: BubblingEventHandler | null | undefined; + onBubblingEventDefinedInlineNullOptionalBoth?: BubblingEventHandler | null | undefined; + onBubblingEventDefinedInlineNullWithPaperName?: BubblingEventHandler< + undefined, + 'paperBubblingEventDefinedInlineNullWithPaperName' + > | null | undefined; +} + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; +const PROPS_AND_EVENTS_TYPES_EXPORTED = ` +/** + * 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 + */ + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; +import type { + BubblingEventHandler, + DirectEventHandler, +} from 'CodegenTypes'; + +export type EventInFile = Readonly<{ + ${EVENT_DEFINITION} +}>; + +export interface ModuleProps extends ViewProps { + // No props + + // Events defined inline + onBubblingEventDefinedInline: BubblingEventHandler; + onBubblingEventDefinedInlineWithPaperName: BubblingEventHandler< + EventInFile, + 'paperBubblingEventDefinedInlineWithPaperName' + >; + onDirectEventDefinedInline: DirectEventHandler; + onDirectEventDefinedInlineWithPaperName: DirectEventHandler< + EventInFile, + 'paperDirectEventDefinedInlineWithPaperName' + >; +} + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; +const PROPS_AS_EXTERNAL_TYPES = ` +/** + * 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 + */ + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {HostComponent} from 'react-native'; + +export type String = string; +export type AnotherArray = ReadonlyArray; + +export interface ModuleProps { + disable: String; + array: AnotherArray; +} + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; +const COMMANDS_DEFINED_WITH_ALL_TYPES = ` +/** + * 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 + */ + + const codegenNativeCommands = require('codegenNativeCommands'); + const codegenNativeComponent = require('codegenNativeComponent'); + + import type {Int32, Double, Float} from 'CodegenTypes'; + import type {RootTag} from 'RCTExport'; + import type {ViewProps} from 'ViewPropTypes'; + import type {HostComponent} from 'react-native'; + + +export interface ModuleProps extends ViewProps { + // No props or events +} + +type NativeType = HostComponent; + + interface NativeCommands { + readonly handleRootTag: (viewRef: React.ElementRef, rootTag: RootTag) => void; + readonly hotspotUpdate: (viewRef: React.ElementRef, x: Int32, y: Int32) => void; + scrollTo( + viewRef: React.ElementRef, + x: Float, + y: Int32, + z: Double, + animated: boolean, + ): void; + } + + export const Commands = codegenNativeCommands({ + supportedCommands: ['handleRootTag', 'hotspotUpdate', 'scrollTo'], + }); + +export default codegenNativeComponent( + 'Module', +) as NativeType; +`; +const COMMANDS_WITH_EXTERNAL_TYPES = ` +/** + * 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 + */ + +const codegenNativeCommands = require('codegenNativeCommands'); +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {Int32} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +export type Boolean = boolean; +export type Int = Int32; +export type Void = void; + +export interface ModuleProps extends ViewProps { + // No props or events +} + +type NativeType = HostComponent; + +export type ScrollTo = ( + viewRef: React.ElementRef, + y: Int, + animated: Boolean, +) => Void; +export type AddOverlays = ( + viewRef: React.ElementRef, + overlayColorsReadOnly: ReadOnlyArray, + overlayColorsArray: Array, + overlayColorsArrayAnnotation: string[], +) => Void; + +interface NativeCommands { + readonly scrollTo: ScrollTo; + readonly addOverlays: AddOverlays; +} + +export const Commands = codegenNativeCommands({ + supportedCommands: ['scrollTo', 'addOverlays'], +}); + +export default codegenNativeComponent('Module') as NativeType; + +`; +const COMMANDS_EVENTS_TYPES_EXPORTED = ` +/** + * 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 + */ + +import type { + BubblingEventHandler, + DirectEventHandler, +} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +export type EventInFile = Readonly<{ + ${EVENT_DEFINITION} +}>; + +export type Boolean = boolean; +export type Int = Int32; +export type Void = void; + +export interface ModuleProps extends ViewProps { + // No props + + // Events defined inline + onBubblingEventDefinedInline: BubblingEventHandler, + onBubblingEventDefinedInlineWithPaperName: BubblingEventHandler, + onDirectEventDefinedInline: DirectEventHandler, + onDirectEventDefinedInlineWithPaperName: DirectEventHandler, +} + +// Add state here +export interface ModuleNativeState { + boolean_required: boolean, + boolean_optional_key?: WithDefault, + boolean_optional_both?: WithDefault, +} + +type NativeType = HostComponent; + +export type ScrollTo = (viewRef: React.ElementRef, y: Int, animated: Boolean) => Void; + +interface NativeCommands { + readonly scrollTo: ScrollTo; +} + +export const Commands = codegenNativeCommands({ + supportedCommands: ['scrollTo'] +}); + +export default codegenNativeComponent( + 'Module', +) as NativeType; +`; +const PROPS_AND_EVENTS_WITH_INTERFACES = ` +import type { + BubblingEventHandler, + DirectEventHandler, + Int32, +} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +export interface Base1 { + readonly x: string; +} + +export interface Base2 { + readonly y: Int32; +} + +export interface Derived extends Base1, Base2 { + readonly z: boolean; +} + +export interface ModuleProps extends ViewProps { + // Props + ordinary_prop: Derived; + readonly_prop: Readonly; + ordinary_array_prop?: readonly Derived[]; + readonly_array_prop?: readonly Readonly[]; + ordinary_nested_array_prop?: readonly Derived[][]; + readonly_nested_array_prop?: readonly Readonly[][]; + + // Events + onDirect: DirectEventHandler; + onBubbling: BubblingEventHandler>; +} + +export default codegenNativeComponent('Module', { + interfaceOnly: true, + paperComponentName: 'RCTModule', +}) as HostComponent; +`; +module.exports = { + ALL_PROP_TYPES_NO_EVENTS, + ARRAY_PROP_TYPES_NO_EVENTS, + ARRAY2_PROP_TYPES_NO_EVENTS, + OBJECT_PROP_TYPES_NO_EVENTS, + PROPS_ALIASED_LOCALLY, + ONE_OF_EACH_PROP_EVENT_DEFAULT_AND_OPTIONS, + ONE_OF_EACH_PROP_EVENT_DEFAULT_AND_OPTIONS_NO_CAST, + NO_PROPS_EVENTS_ONLY_DEPRECATED_VIEW_CONFIG_NAME_OPTION, + EVENTS_DEFINED_INLINE_WITH_ALL_TYPES, + EVENTS_DEFINED_AS_NULL_INLINE, + PROPS_AND_EVENTS_TYPES_EXPORTED, + COMMANDS_EVENTS_TYPES_EXPORTED, + COMMANDS_DEFINED_WITH_ALL_TYPES, + PROPS_AS_EXTERNAL_TYPES, + COMMANDS_WITH_EXTERNAL_TYPES, + PROPS_AND_EVENTS_WITH_INTERFACES, +}; diff --git a/packages/react-native-codegen/lib/parsers/typescript/components/__test_fixtures__/fixtures.js.flow b/packages/react-native-codegen/lib/parsers/typescript/components/__test_fixtures__/fixtures.js.flow new file mode 100644 index 00000000000000..ed237a0b6c9ef8 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/typescript/components/__test_fixtures__/fixtures.js.flow @@ -0,0 +1,1252 @@ +/** + * 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-local + * @format + */ + +// @licenselint-loose-mode + +'use strict'; + +const EVENT_DEFINITION = ` + boolean_required: boolean; + boolean_optional_key?: boolean; + boolean_optional_value: boolean | null | undefined; + boolean_optional_both?: boolean | null | undefined; + + string_required: string; + string_optional_key?: (string); + string_optional_value: (string) | null | undefined; + string_optional_both?: (string | null | undefined); + + double_required: Double; + double_optional_key?: Double; + double_optional_value: Double | null | undefined; + double_optional_both?: Double | null | undefined; + + float_required: Float; + float_optional_key?: Float; + float_optional_value: Float | null | undefined; + float_optional_both?: Float | null | undefined; + + int32_required: Int32; + int32_optional_key?: Int32; + int32_optional_value: Int32 | null | undefined; + int32_optional_both?: Int32 | null | undefined; + + enum_required: 'small' | 'large'; + enum_optional_key?: 'small' | 'large'; + enum_optional_value: ('small' | 'large') | null | undefined; + enum_optional_both?: ('small' | 'large') | null | undefined; + + object_required: { + boolean_required: boolean; + }; + + object_optional_key?: { + string_optional_key?: string; + }; + + object_optional_value: { + float_optional_value: Float | null | undefined; + } | null | undefined; + + object_optional_both?: { + int32_optional_both?: Int32 | null | undefined; + } | null | undefined; + + object_required_nested_2_layers: { + object_optional_nested_1_layer?: { + boolean_required: Int32; + string_optional_key?: string; + double_optional_value: Double | null | undefined; + float_optional_value: Float | null | undefined; + int32_optional_both?: Int32 | null | undefined; + } | null | undefined; + }; + + object_readonly_required: Readonly<{ + boolean_required: boolean; + }>; + + object_readonly_optional_key?: Readonly<{ + string_optional_key?: string; + }>; + + object_readonly_optional_value: Readonly<{ + float_optional_value: Float | null | undefined; + }> | null | undefined; + + object_readonly_optional_both?: Readonly<{ + int32_optional_both?: Int32 | null | undefined; + }> | null | undefined; + + boolean_array_required: boolean[]; + boolean_array_optional_key?: boolean[]; + boolean_array_optional_value: boolean[] | null | undefined; + boolean_array_optional_both?: boolean[] | null | undefined; + + string_array_required: string[]; + string_array_optional_key?: (string[]); + string_array_optional_value: (string[]) | null | undefined; + string_array_optional_both?: (string[] | null | undefined); + + double_array_required: Double[]; + double_array_optional_key?: Double[]; + double_array_optional_value: Double[] | null | undefined; + double_array_optional_both?: Double[] | null | undefined; + + float_array_required: Float[]; + float_array_optional_key?: Float[]; + float_array_optional_value: Float[] | null | undefined; + float_array_optional_both?: Float[] | null | undefined; + + int32_array_required: Int32[]; + int32_array_optional_key?: Int32[]; + int32_array_optional_value: Int32[] | null | undefined; + int32_array_optional_both?: Int32[] | null | undefined; + + enum_array_required: ('small' | 'large')[]; + enum_array_optional_key?: ('small' | 'large')[]; + enum_array_optional_value: ('small' | 'large')[] | null | undefined; + enum_array_optional_both?: ('small' | 'large')[] | null | undefined; + + object_array_required: { + boolean_required: boolean; + }[]; + + object_array_optional_key?: { + string_optional_key?: string; + }[]; + + object_array_optional_value: { + float_optional_value: Float | null | undefined; + }[] | null | undefined; + + object_array_optional_both?: { + int32_optional_both?: Int32 | null | undefined; + }[] | null | undefined; + + int32_array_array_required: Int32[][]; + int32_array_array_optional_key?: Int32[][]; + int32_array_array_optional_value: Int32[][] | null | undefined; + int32_array_array_optional_both?: Int32[][] | null | undefined; +`; + +const ONE_OF_EACH_PROP_EVENT_DEFAULT_AND_OPTIONS = ` +/** + * 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'; + +import type { + BubblingEventHandler, + DirectEventHandler, + WithDefault, +} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +export interface ModuleProps extends ViewProps { + // Props + boolean_default_true_optional_both?: WithDefault; + + // Events + onDirectEventDefinedInlineNull: DirectEventHandler; + onBubblingEventDefinedInlineNull: BubblingEventHandler; +} + +export default codegenNativeComponent('Module', { + interfaceOnly: true, + paperComponentName: 'RCTModule', +}) as HostComponent; +`; + +const ONE_OF_EACH_PROP_EVENT_DEFAULT_AND_OPTIONS_NO_CAST = ` +/** + * 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 + */ + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type { + BubblingEventHandler, + DirectEventHandler, + WithDefault, +} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +export interface ModuleProps extends ViewProps { + // Props + boolean_default_true_optional_both?: WithDefault; + + // Events + onDirectEventDefinedInlineNull: DirectEventHandler; + onBubblingEventDefinedInlineNull: BubblingEventHandler; +} + +export default codegenNativeComponent('Module', { + interfaceOnly: true, + excludedPlatforms: ['android'], + paperComponentName: 'RCTModule', +}) as HostComponent; +`; + +const NO_PROPS_EVENTS_ONLY_DEPRECATED_VIEW_CONFIG_NAME_OPTION = ` +/** + * 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 + */ + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +export interface ModuleProps extends ViewProps { + +} + +export default codegenNativeComponent('Module', { + deprecatedViewConfigName: 'DeprecateModuleName', +}) as HostComponent; +`; + +const ALL_PROP_TYPES_NO_EVENTS = ` +/** + * 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 codegenNativeComponent = require('codegenNativeComponent'); + +import type {Int32, Double, Float, WithDefault, UnsafeMixed} from 'CodegenTypes'; +import type {ImageSource} from 'ImageSource'; +import type { + ColorValue, + ColorArrayValue, + PointValue, + EdgeInsetsValue, + DimensionValue, +} from 'StyleSheetTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +export interface ModuleProps extends ViewProps { + // Props + // Boolean props + boolean_required: boolean; + boolean_optional_key?: WithDefault; + boolean_optional_both?: WithDefault; + + // Boolean props, null default + boolean_null_optional_key?: WithDefault; + boolean_null_optional_both?: WithDefault; + + // String props + string_required: string; + string_optional_key?: WithDefault; + string_optional_both?: WithDefault; + + // String props, null default + string_null_optional_key?: WithDefault; + string_null_optional_both?: WithDefault; + + // Stringish props + stringish_required: Stringish; + stringish_optional_key?: WithDefault; + stringish_optional_both?: WithDefault; + + // Stringish props, null default + stringish_null_optional_key?: WithDefault; + stringish_null_optional_both?: WithDefault; + + // Double props + double_required: Double; + double_optional_key?: WithDefault; + double_optional_both?: WithDefault; + + // Float props + float_required: Float; + float_optional_key?: WithDefault; + float_optional_both?: WithDefault; + + // Float props, null default + float_null_optional_key?: WithDefault; + float_null_optional_both?: WithDefault; + + // Int32 props + int32_required: Int32; + int32_optional_key?: WithDefault; + int32_optional_both?: WithDefault; + + // String enum props + enum_optional_key?: WithDefault<'small' | 'large', 'small'>; + enum_optional_both?: WithDefault<'small' | 'large', 'small'>; + + // Int enum props + int_enum_optional_key?: WithDefault<0 | 1, 0>; + + // Object props + object_optional_key?: Readonly<{prop: string}>; + object_optional_both?: Readonly<{prop: string} | null | undefined>; + object_optional_value: Readonly<{prop: string} | null | undefined>; + + // ImageSource props + image_required: ImageSource; + image_optional_value: ImageSource | null | undefined; + image_optional_both?: ImageSource | null | undefined; + + // ColorValue props + color_required: ColorValue; + color_optional_key?: ColorValue; + color_optional_value: ColorValue | null | undefined; + color_optional_both?: ColorValue | null | undefined; + + // ColorArrayValue props + color_array_required: ColorArrayValue; + color_array_optional_key?: ColorArrayValue; + color_array_optional_value: ColorArrayValue | null | undefined; + color_array_optional_both?: ColorArrayValue | null | undefined; + + // ProcessedColorValue props + processed_color_required: ProcessedColorValue; + processed_color_optional_key?: ProcessedColorValue; + processed_color_optional_value: ProcessedColorValue | null | undefined; + processed_color_optional_both?: ProcessedColorValue | null | undefined; + + // PointValue props + point_required: PointValue; + point_optional_key?: PointValue; + point_optional_value: PointValue | null | undefined; + point_optional_both?: PointValue | null | undefined; + + // EdgeInsets props + insets_required: EdgeInsetsValue; + insets_optional_key?: EdgeInsetsValue; + insets_optional_value: EdgeInsetsValue | null | undefined; + insets_optional_both?: EdgeInsetsValue | null | undefined; + + + // DimensionValue props + dimension_required: DimensionValue; + dimension_optional_key?: DimensionValue; + dimension_optional_value: DimensionValue | null | undefined; + dimension_optional_both?: DimensionValue | null | undefined; + + // Mixed props + mixed_required: UnsafeMixed, + mixed_optional_key?: UnsafeMixed, +} + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; + +const ARRAY_PROP_TYPES_NO_EVENTS = ` +/** + * 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 codegenNativeComponent = require('codegenNativeComponent'); + +import type {Int32, Double, Float, WithDefault} from 'CodegenTypes'; +import type {ImageSource} from 'ImageSource'; +import type { + ColorValue, + ColorArrayValue, + PointValue, + EdgeInsetsValue, + DimensionValue, +} from 'StyleSheetTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +type ObjectType = Readonly<{prop: string}>; +type ArrayObjectType = ReadonlyArray>; + +export interface ModuleProps extends ViewProps { + // Props + // Boolean props + array_boolean_required: ReadonlyArray; + array_boolean_optional_key?: ReadonlyArray; + array_boolean_optional_value: ReadonlyArray | null | undefined; + array_boolean_optional_both?: ReadonlyArray | null | undefined; + + // String props + array_string_required: ReadonlyArray; + array_string_optional_key?: ReadonlyArray; + array_string_optional_value: ReadonlyArray | null | undefined; + array_string_optional_both?: ReadonlyArray | null | undefined; + + // Double props + array_double_required: ReadonlyArray; + array_double_optional_key?: ReadonlyArray; + array_double_optional_value: ReadonlyArray | null | undefined; + array_double_optional_both?: ReadonlyArray | null | undefined; + + // Float props + array_float_required: ReadonlyArray; + array_float_optional_key?: ReadonlyArray; + array_float_optional_value: ReadonlyArray | null | undefined; + array_float_optional_both?: ReadonlyArray | null | undefined; + + // Int32 props + array_int32_required: ReadonlyArray; + array_int32_optional_key?: ReadonlyArray; + array_int32_optional_value: ReadonlyArray | null | undefined; + array_int32_optional_both?: ReadonlyArray | null | undefined; + + // String enum props + array_enum_optional_key?: WithDefault< + ReadonlyArray<'small' | 'large'>, + 'small' + >; + array_enum_optional_both?: WithDefault< + ReadonlyArray<'small' | 'large'>, + 'small' + >; + + // ImageSource props + array_image_required: ReadonlyArray; + array_image_optional_key?: ReadonlyArray; + array_image_optional_value: ReadonlyArray | null | undefined; + array_image_optional_both?: ReadonlyArray | null | undefined; + + // ColorValue props + array_color_required: ReadonlyArray; + array_color_optional_key?: ReadonlyArray; + array_color_optional_value: ReadonlyArray | null | undefined; + array_color_optional_both?: ReadonlyArray | null | undefined; + + // PointValue props + array_point_required: ReadonlyArray; + array_point_optional_key?: ReadonlyArray; + array_point_optional_value: ReadonlyArray | null | undefined; + array_point_optional_both?: ReadonlyArray | null | undefined; + + // EdgeInsetsValue props + array_insets_required: ReadonlyArray; + array_insets_optional_key?: ReadonlyArray; + array_insets_optional_value: ReadonlyArray | null | undefined; + array_insets_optional_both?: ReadonlyArray | null | undefined; + + // DimensionValue props + array_dimension_required: ReadonlyArray; + array_dimension_optional_key?: ReadonlyArray; + array_dimension_optional_value: ReadonlyArray | null | undefined; + array_dimension_optional_both?: ReadonlyArray | null | undefined; + + // Object props + array_object_required: ReadonlyArray>; + array_object_optional_key?: ReadonlyArray>; + array_object_optional_value: ArrayObjectType | null | undefined; + array_object_optional_both?: ReadonlyArray | null | undefined; + + // Nested array object types + array_of_array_object_required: ReadonlyArray< + Readonly<{ + // This needs to be the same name as the top level array above + array_object_required: ReadonlyArray>; + }> + >; + array_of_array_object_optional_key?: ReadonlyArray< + Readonly<{ + // This needs to be the same name as the top level array above + array_object_optional_key: ReadonlyArray>; + }> + >; + array_of_array_object_optional_value: ReadonlyArray< + Readonly<{ + // This needs to be the same name as the top level array above + array_object_optional_value: ReadonlyArray< + Readonly<{prop: string | null | undefined}> + >; + }> + > | null | undefined; + array_of_array_object_optional_both?: ReadonlyArray< + Readonly<{ + // This needs to be the same name as the top level array above + array_object_optional_both: ReadonlyArray< + Readonly<{prop?: string | null | undefined}> + >; + }> + > | null | undefined; + + // Nested array of array of object types + array_of_array_of_object_required: ReadonlyArray< + ReadonlyArray< + Readonly<{ + prop: string; + }> + > + >; + + // Nested array of array of object types (in file) + array_of_array_of_object_required_in_file: ReadonlyArray< + ReadonlyArray + >; + + // Nested array of array of object types (with spread) + array_of_array_of_object_required_with_spread: ReadonlyArray< + ReadonlyArray< + Readonly, + >, + >, +} + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; + +const ARRAY2_PROP_TYPES_NO_EVENTS = ` +/** + * 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 codegenNativeComponent = require('codegenNativeComponent'); + +import type {Int32, Double, Float, WithDefault} from 'CodegenTypes'; +import type {ImageSource} from 'ImageSource'; +import type { + ColorValue, + ColorArrayValue, + PointValue, + EdgeInsetsValue, +} from 'StyleSheetTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +type ObjectType = Readonly<{prop: string}>; +type ArrayObjectType = readonly Readonly<{prop: string}>[]; + +export interface ModuleProps extends ViewProps { + // Props + // Boolean props + array_boolean_required: readonly boolean[]; + array_boolean_optional_key?: readonly boolean[]; + array_boolean_optional_value: readonly boolean[] | null | undefined; + array_boolean_optional_both?: readonly boolean[] | null | undefined; + + // String props + array_string_required: readonly string[]; + array_string_optional_key?: readonly string[]; + array_string_optional_value: readonly string[] | null | undefined; + array_string_optional_both?: readonly string[] | null | undefined; + + // Double props + array_double_required: readonly Double[]; + array_double_optional_key?: readonly Double[]; + array_double_optional_value: readonly Double[] | null | undefined; + array_double_optional_both?: readonly Double[] | null | undefined; + + // Float props + array_float_required: readonly Float[]; + array_float_optional_key?: readonly Float[]; + array_float_optional_value: readonly Float[] | null | undefined; + array_float_optional_both?: readonly Float[] | null | undefined; + + // Int32 props + array_int32_required: readonly Int32[]; + array_int32_optional_key?: readonly Int32[]; + array_int32_optional_value: readonly Int32[] | null | undefined; + array_int32_optional_both?: readonly Int32[] | null | undefined; + + // String enum props + array_enum_optional_key?: WithDefault< + readonly ('small' | 'large')[], + 'small' + >; + array_enum_optional_both?: WithDefault< + readonly ('small' | 'large')[], + 'small' + >; + + // ImageSource props + array_image_required: readonly ImageSource[]; + array_image_optional_key?: readonly ImageSource[]; + array_image_optional_value: readonly ImageSource[] | null | undefined; + array_image_optional_both?: readonly ImageSource[] | null | undefined; + + // ColorValue props + array_color_required: readonly ColorValue[]; + array_color_optional_key?: readonly ColorValue[]; + array_color_optional_value: readonly ColorValue[] | null | undefined; + array_color_optional_both?: readonly ColorValue[] | null | undefined; + + // PointValue props + array_point_required: readonly PointValue[]; + array_point_optional_key?: readonly PointValue[]; + array_point_optional_value: readonly PointValue[] | null | undefined; + array_point_optional_both?: readonly PointValue[] | null | undefined; + + // EdgeInsetsValue props + array_insets_required: readonly EdgeInsetsValue[]; + array_insets_optional_key?: readonly EdgeInsetsValue[]; + array_insets_optional_value: readonly EdgeInsetsValue[] | null | undefined; + array_insets_optional_both?: readonly EdgeInsetsValue[] | null | undefined; + + // Object props + array_object_required: readonly Readonly<{prop: string}>[]; + array_object_optional_key?: readonly Readonly<{prop: string}>[]; + array_object_optional_value: ArrayObjectType | null | undefined; + array_object_optional_both?: readonly ObjectType[] | null | undefined; + + // Nested array object types + array_of_array_object_required: readonly Readonly<{ + // This needs to be the same name as the top level array above + array_object_required: readonly Readonly<{prop: string}>[]; + }>[]; + array_of_array_object_optional_key?: readonly Readonly<{ + // This needs to be the same name as the top level array above + array_object_optional_key: readonly Readonly<{prop?: string}>[]; + }>[]; + array_of_array_object_optional_value: readonly Readonly<{ + // This needs to be the same name as the top level array above + array_object_optional_value: readonly Readonly<{prop: string | null | undefined}>[]; + }>[] | null | undefined; + array_of_array_object_optional_both?: readonly Readonly<{ + // This needs to be the same name as the top level array above + array_object_optional_both: readonly Readonly<{prop?: string | null | undefined}>[]; + }>[] | null | undefined; + + // Nested array of array of object types + array_of_array_of_object_required: readonly Readonly<{ + prop: string; + }>[][]; + + // Nested array of array of object types (in file) + array_of_array_of_object_required_in_file: readonly ObjectType[][]; + + // Nested array of array of object types (with spread) + array_of_array_of_object_required_with_spread: readonly Readonly[][]; +} + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; + +const OBJECT_PROP_TYPES_NO_EVENTS = ` +/** + * 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 codegenNativeComponent = require('codegenNativeComponent'); + +import type {Int32, Double, Float, WithDefault} from 'CodegenTypes'; +import type {ImageSource} from 'ImageSource'; +import type { + ColorValue, + ColorArrayValue, + PointValue, + EdgeInsetsValue, + DimensionValue, +} from 'StyleSheetTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +export interface ModuleProps extends ViewProps { + // Props + // Boolean props + boolean_required: Readonly<{prop: boolean}>; + boolean_optional: Readonly<{prop?: WithDefault}>; + + // String props + string_required: Readonly<{prop: string}>; + string_optional: Readonly<{prop?: WithDefault}>; + + // Double props + double_required: Readonly<{prop: Double}>; + double_optional: Readonly<{prop?: WithDefault}>; + + // Float props + float_required: Readonly<{prop: Float}>; + float_optional: Readonly<{prop?: WithDefault}>; + + // Int32 props + int_required: Readonly<{prop: Int32}>; + int_optional: Readonly<{prop?: WithDefault}>; + + // String enum props + enum_optional: Readonly<{ + prop?: WithDefault, 'small'>; + }>; + + // ImageSource props + image_required: Readonly<{prop: ImageSource}>; + image_optional_key: Readonly<{prop?: ImageSource}>; + image_optional_value: Readonly<{prop: ImageSource | null | undefined}>; + image_optional_both: Readonly<{prop?: ImageSource | null | undefined}>; + + // ColorValue props + color_required: Readonly<{prop: ColorValue}>; + color_optional_key: Readonly<{prop?: ColorValue}>; + color_optional_value: Readonly<{prop: ColorValue | null | undefined}>; + color_optional_both: Readonly<{prop?: ColorValue | null | undefined}>; + + // ProcessedColorValue props + processed_color_required: Readonly<{prop: ProcessedColorValue}>; + processed_color_optional_key: Readonly<{prop?: ProcessedColorValue}>; + processed_color_optional_value: Readonly<{ + prop: ProcessedColorValue | null | undefined; + }>; + processed_color_optional_both: Readonly<{ + prop?: ProcessedColorValue | null | undefined; + }>; + + // PointValue props + point_required: Readonly<{prop: PointValue}>; + point_optional_key: Readonly<{prop?: PointValue}>; + point_optional_value: Readonly<{prop: PointValue | null | undefined}>; + point_optional_both: Readonly<{prop?: PointValue | null | undefined}>; + + // EdgeInsetsValue props + insets_required: Readonly<{prop: EdgeInsetsValue}>; + insets_optional_key: Readonly<{prop?: EdgeInsetsValue}>; + insets_optional_value: Readonly<{prop: EdgeInsetsValue | null | undefined}>; + insets_optional_both: Readonly<{prop?: EdgeInsetsValue | null | undefined}>; + + // DimensionValue props + dimension_required: Readonly<{prop: DimensionValue}>; + dimension_optional_key: Readonly<{prop?: DimensionValue}>; + dimension_optional_value: Readonly<{prop: DimensionValue | null | undefined}>; + dimension_optional_both: Readonly<{prop?: DimensionValue | null | undefined}>; + + // Nested object props + object_required: Readonly<{prop: Readonly<{nestedProp: string}>}>; + object_optional_key?: Readonly<{prop: Readonly<{nestedProp: string}>}>; + object_optional_value: Readonly<{ + prop: Readonly<{nestedProp: string}>; + }> | null | undefined; + object_optional_both?: Readonly<{ + prop: Readonly<{nestedProp: string}>; + }> | null | undefined; +} + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; + +const PROPS_ALIASED_LOCALLY = ` +/** + * 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'; + +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +type DeepSpread = Readonly<{ + otherStringProp: string; +}>; + +export interface PropsInFile extends DeepSpread { + isEnabled: boolean; + label: string; +} + +type ReadOnlyPropsInFile = Readonly; + +export interface ModuleProps extends ViewProps, ReadOnlyPropsInFile { + localType: ReadOnlyPropsInFile; + localArr: ReadonlyArray; +} + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; + +const EVENTS_DEFINED_INLINE_WITH_ALL_TYPES = ` +/** + * 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 + */ + +import type {HostComponent} from 'react-native'; +const codegenNativeComponent = require('codegenNativeComponent'); + +import type { + Int32, + Double, + Float, + BubblingEventHandler, + DirectEventHandler, +} from 'CodegenTypes'; + +import type {ViewProps} from 'ViewPropTypes'; + +export interface ModuleProps extends ViewProps { + // No Props + + // Events + onDirectEventDefinedInline: DirectEventHandler< + Readonly<{ + ${EVENT_DEFINITION} + }> + >; + + onDirectEventDefinedInlineOptionalKey?: (DirectEventHandler< + Readonly<{ + ${EVENT_DEFINITION} + }> + >); + + onDirectEventDefinedInlineOptionalValue: (DirectEventHandler< + Readonly<{ + ${EVENT_DEFINITION} + }> + >) | null | undefined; + + onDirectEventDefinedInlineOptionalBoth?: (DirectEventHandler< + Readonly<{ + ${EVENT_DEFINITION} + }> + > | null | undefined); + + onDirectEventDefinedInlineWithPaperName?: DirectEventHandler< + Readonly<{ + ${EVENT_DEFINITION} + }>, + 'paperDirectEventDefinedInlineWithPaperName' + > | null | undefined; + + onBubblingEventDefinedInline: BubblingEventHandler< + Readonly<{ + ${EVENT_DEFINITION} + }> + >; + + onBubblingEventDefinedInlineOptionalKey?: BubblingEventHandler< + Readonly<{ + ${EVENT_DEFINITION} + }> + >; + + onBubblingEventDefinedInlineOptionalValue: BubblingEventHandler< + Readonly<{ + ${EVENT_DEFINITION} + }> + > | null | undefined; + + onBubblingEventDefinedInlineOptionalBoth?: BubblingEventHandler< + Readonly<{ + ${EVENT_DEFINITION} + }> + > | null | undefined; + + onBubblingEventDefinedInlineWithPaperName?: BubblingEventHandler< + Readonly<{ + ${EVENT_DEFINITION} + }>, + 'paperBubblingEventDefinedInlineWithPaperName' + > | null | undefined; +} + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; + +const EVENTS_DEFINED_AS_NULL_INLINE = ` +/** + * 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 codegenNativeComponent = require('codegenNativeComponent'); + +import type {BubblingEventHandler, DirectEventHandler} from 'CodegenTypese'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +export interface ModuleProps extends ViewProps { + // No props + + // Events defined inline + onDirectEventDefinedInlineNull: DirectEventHandler; + onDirectEventDefinedInlineNullOptionalKey?: DirectEventHandler; + onDirectEventDefinedInlineNullOptionalValue: DirectEventHandler | null | undefined; + onDirectEventDefinedInlineNullOptionalBoth?: DirectEventHandler; + onDirectEventDefinedInlineNullWithPaperName?: DirectEventHandler< + null, + 'paperDirectEventDefinedInlineNullWithPaperName' + > | null | undefined; + + onBubblingEventDefinedInlineNull: BubblingEventHandler; + onBubblingEventDefinedInlineNullOptionalKey?: BubblingEventHandler; + onBubblingEventDefinedInlineNullOptionalValue: BubblingEventHandler | null | undefined; + onBubblingEventDefinedInlineNullOptionalBoth?: BubblingEventHandler | null | undefined; + onBubblingEventDefinedInlineNullWithPaperName?: BubblingEventHandler< + undefined, + 'paperBubblingEventDefinedInlineNullWithPaperName' + > | null | undefined; +} + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; + +const PROPS_AND_EVENTS_TYPES_EXPORTED = ` +/** + * 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 + */ + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; +import type { + BubblingEventHandler, + DirectEventHandler, +} from 'CodegenTypes'; + +export type EventInFile = Readonly<{ + ${EVENT_DEFINITION} +}>; + +export interface ModuleProps extends ViewProps { + // No props + + // Events defined inline + onBubblingEventDefinedInline: BubblingEventHandler; + onBubblingEventDefinedInlineWithPaperName: BubblingEventHandler< + EventInFile, + 'paperBubblingEventDefinedInlineWithPaperName' + >; + onDirectEventDefinedInline: DirectEventHandler; + onDirectEventDefinedInlineWithPaperName: DirectEventHandler< + EventInFile, + 'paperDirectEventDefinedInlineWithPaperName' + >; +} + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; + +const PROPS_AS_EXTERNAL_TYPES = ` +/** + * 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 + */ + +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {HostComponent} from 'react-native'; + +export type String = string; +export type AnotherArray = ReadonlyArray; + +export interface ModuleProps { + disable: String; + array: AnotherArray; +} + +export default codegenNativeComponent( + 'Module', +) as HostComponent; +`; + +const COMMANDS_DEFINED_WITH_ALL_TYPES = ` +/** + * 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 + */ + + const codegenNativeCommands = require('codegenNativeCommands'); + const codegenNativeComponent = require('codegenNativeComponent'); + + import type {Int32, Double, Float} from 'CodegenTypes'; + import type {RootTag} from 'RCTExport'; + import type {ViewProps} from 'ViewPropTypes'; + import type {HostComponent} from 'react-native'; + + +export interface ModuleProps extends ViewProps { + // No props or events +} + +type NativeType = HostComponent; + + interface NativeCommands { + readonly handleRootTag: (viewRef: React.ElementRef, rootTag: RootTag) => void; + readonly hotspotUpdate: (viewRef: React.ElementRef, x: Int32, y: Int32) => void; + scrollTo( + viewRef: React.ElementRef, + x: Float, + y: Int32, + z: Double, + animated: boolean, + ): void; + } + + export const Commands = codegenNativeCommands({ + supportedCommands: ['handleRootTag', 'hotspotUpdate', 'scrollTo'], + }); + +export default codegenNativeComponent( + 'Module', +) as NativeType; +`; + +const COMMANDS_WITH_EXTERNAL_TYPES = ` +/** + * 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 + */ + +const codegenNativeCommands = require('codegenNativeCommands'); +const codegenNativeComponent = require('codegenNativeComponent'); + +import type {Int32} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +export type Boolean = boolean; +export type Int = Int32; +export type Void = void; + +export interface ModuleProps extends ViewProps { + // No props or events +} + +type NativeType = HostComponent; + +export type ScrollTo = ( + viewRef: React.ElementRef, + y: Int, + animated: Boolean, +) => Void; +export type AddOverlays = ( + viewRef: React.ElementRef, + overlayColorsReadOnly: ReadOnlyArray, + overlayColorsArray: Array, + overlayColorsArrayAnnotation: string[], +) => Void; + +interface NativeCommands { + readonly scrollTo: ScrollTo; + readonly addOverlays: AddOverlays; +} + +export const Commands = codegenNativeCommands({ + supportedCommands: ['scrollTo', 'addOverlays'], +}); + +export default codegenNativeComponent('Module') as NativeType; + +`; + +const COMMANDS_EVENTS_TYPES_EXPORTED = ` +/** + * 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 + */ + +import type { + BubblingEventHandler, + DirectEventHandler, +} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +export type EventInFile = Readonly<{ + ${EVENT_DEFINITION} +}>; + +export type Boolean = boolean; +export type Int = Int32; +export type Void = void; + +export interface ModuleProps extends ViewProps { + // No props + + // Events defined inline + onBubblingEventDefinedInline: BubblingEventHandler, + onBubblingEventDefinedInlineWithPaperName: BubblingEventHandler, + onDirectEventDefinedInline: DirectEventHandler, + onDirectEventDefinedInlineWithPaperName: DirectEventHandler, +} + +// Add state here +export interface ModuleNativeState { + boolean_required: boolean, + boolean_optional_key?: WithDefault, + boolean_optional_both?: WithDefault, +} + +type NativeType = HostComponent; + +export type ScrollTo = (viewRef: React.ElementRef, y: Int, animated: Boolean) => Void; + +interface NativeCommands { + readonly scrollTo: ScrollTo; +} + +export const Commands = codegenNativeCommands({ + supportedCommands: ['scrollTo'] +}); + +export default codegenNativeComponent( + 'Module', +) as NativeType; +`; + +const PROPS_AND_EVENTS_WITH_INTERFACES = ` +import type { + BubblingEventHandler, + DirectEventHandler, + Int32, +} from 'CodegenTypes'; +import type {ViewProps} from 'ViewPropTypes'; +import type {HostComponent} from 'react-native'; + +const codegenNativeComponent = require('codegenNativeComponent'); + +export interface Base1 { + readonly x: string; +} + +export interface Base2 { + readonly y: Int32; +} + +export interface Derived extends Base1, Base2 { + readonly z: boolean; +} + +export interface ModuleProps extends ViewProps { + // Props + ordinary_prop: Derived; + readonly_prop: Readonly; + ordinary_array_prop?: readonly Derived[]; + readonly_array_prop?: readonly Readonly[]; + ordinary_nested_array_prop?: readonly Derived[][]; + readonly_nested_array_prop?: readonly Readonly[][]; + + // Events + onDirect: DirectEventHandler; + onBubbling: BubblingEventHandler>; +} + +export default codegenNativeComponent('Module', { + interfaceOnly: true, + paperComponentName: 'RCTModule', +}) as HostComponent; +`; + +module.exports = { + ALL_PROP_TYPES_NO_EVENTS, + ARRAY_PROP_TYPES_NO_EVENTS, + ARRAY2_PROP_TYPES_NO_EVENTS, + OBJECT_PROP_TYPES_NO_EVENTS, + PROPS_ALIASED_LOCALLY, + ONE_OF_EACH_PROP_EVENT_DEFAULT_AND_OPTIONS, + ONE_OF_EACH_PROP_EVENT_DEFAULT_AND_OPTIONS_NO_CAST, + NO_PROPS_EVENTS_ONLY_DEPRECATED_VIEW_CONFIG_NAME_OPTION, + EVENTS_DEFINED_INLINE_WITH_ALL_TYPES, + EVENTS_DEFINED_AS_NULL_INLINE, + PROPS_AND_EVENTS_TYPES_EXPORTED, + COMMANDS_EVENTS_TYPES_EXPORTED, + COMMANDS_DEFINED_WITH_ALL_TYPES, + PROPS_AS_EXTERNAL_TYPES, + COMMANDS_WITH_EXTERNAL_TYPES, + PROPS_AND_EVENTS_WITH_INTERFACES, +}; diff --git a/packages/react-native-codegen/lib/parsers/typescript/components/commands.js b/packages/react-native-codegen/lib/parsers/typescript/components/commands.js new file mode 100644 index 00000000000000..9c76cd65316e41 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/typescript/components/commands.js @@ -0,0 +1,143 @@ +/** + * 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 _require = require('../parseTopLevelType'), + parseTopLevelType = _require.parseTopLevelType; +const _require2 = require('./componentsUtils'), + getPrimitiveTypeAnnotation = _require2.getPrimitiveTypeAnnotation; + +// $FlowFixMe[unclear-type] there's no flowtype for ASTs + +function buildCommandSchemaInternal(name, optional, parameters, types) { + var _firstParam$typeAnnot, _firstParam$typeAnnot2; + const firstParam = parameters[0].typeAnnotation; + if ( + !( + firstParam.typeAnnotation != null && + firstParam.typeAnnotation.type === 'TSTypeReference' && + ((_firstParam$typeAnnot = firstParam.typeAnnotation.typeName.left) === + null || _firstParam$typeAnnot === void 0 + ? void 0 + : _firstParam$typeAnnot.name) === 'React' && + ((_firstParam$typeAnnot2 = firstParam.typeAnnotation.typeName.right) === + null || _firstParam$typeAnnot2 === void 0 + ? void 0 + : _firstParam$typeAnnot2.name) === 'ElementRef' + ) + ) { + throw new Error( + `The first argument of method ${name} must be of type React.ElementRef<>`, + ); + } + const params = parameters.slice(1).map(param => { + const paramName = param.name; + const paramValue = parseTopLevelType( + param.typeAnnotation.typeAnnotation, + types, + ).type; + const type = + paramValue.type === 'TSTypeReference' + ? paramValue.typeName.name + : paramValue.type; + let returnType; + switch (type) { + case 'RootTag': + returnType = { + type: 'ReservedTypeAnnotation', + name: 'RootTag', + }; + break; + case 'TSBooleanKeyword': + case 'Int32': + case 'Double': + case 'Float': + case 'TSStringKeyword': + returnType = getPrimitiveTypeAnnotation(type); + break; + case 'Array': + case 'ReadOnlyArray': + if (!paramValue.type === 'TSTypeReference') { + throw new Error( + 'Array and ReadOnlyArray are TSTypeReference for array', + ); + } + returnType = { + type: 'ArrayTypeAnnotation', + elementType: getPrimitiveTypeAnnotation( + // TODO: T172453752 support complex type annotation for array element + paramValue.typeParameters.params[0].type, + ), + }; + break; + case 'TSArrayType': + returnType = { + type: 'ArrayTypeAnnotation', + elementType: { + // TODO: T172453752 support complex type annotation for array element + type: getPrimitiveTypeAnnotation(paramValue.elementType.type).type, + }, + }; + break; + default: + type; + throw new Error( + `Unsupported param type for method "${name}", param "${paramName}". Found ${type}`, + ); + } + return { + name: paramName, + optional: false, + typeAnnotation: returnType, + }; + }); + return { + name, + optional, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + params, + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + }, + }; +} +function buildCommandSchema(property, types) { + if (property.type === 'TSPropertySignature') { + const topLevelType = parseTopLevelType( + property.typeAnnotation.typeAnnotation, + types, + ); + const name = property.key.name; + const optional = property.optional || topLevelType.optional; + const parameters = topLevelType.type.parameters || topLevelType.type.params; + return buildCommandSchemaInternal(name, optional, parameters, types); + } else { + const name = property.key.name; + const optional = property.optional || false; + const parameters = property.parameters || property.params; + return buildCommandSchemaInternal(name, optional, parameters, types); + } +} +function getCommands(commandTypeAST, types) { + return commandTypeAST + .filter( + property => + property.type === 'TSPropertySignature' || + property.type === 'TSMethodSignature', + ) + .map(property => buildCommandSchema(property, types)) + .filter(Boolean); +} +module.exports = { + getCommands, +}; diff --git a/packages/react-native-codegen/lib/parsers/typescript/components/commands.js.flow b/packages/react-native-codegen/lib/parsers/typescript/components/commands.js.flow new file mode 100644 index 00000000000000..882f7002d9a4b6 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/typescript/components/commands.js.flow @@ -0,0 +1,160 @@ +/** + * 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 { + CommandTypeAnnotation, + NamedShape, +} from '../../../CodegenSchema.js'; +import type {TypeDeclarationMap} from '../../utils'; + +const {parseTopLevelType} = require('../parseTopLevelType'); +const {getPrimitiveTypeAnnotation} = require('./componentsUtils'); + +// $FlowFixMe[unclear-type] there's no flowtype for ASTs +type EventTypeAST = Object; + +function buildCommandSchemaInternal( + name: string, + optional: boolean, + parameters: Array<$FlowFixMe>, + types: TypeDeclarationMap, +): NamedShape { + const firstParam = parameters[0].typeAnnotation; + if ( + !( + firstParam.typeAnnotation != null && + firstParam.typeAnnotation.type === 'TSTypeReference' && + firstParam.typeAnnotation.typeName.left?.name === 'React' && + firstParam.typeAnnotation.typeName.right?.name === 'ElementRef' + ) + ) { + throw new Error( + `The first argument of method ${name} must be of type React.ElementRef<>`, + ); + } + + const params = parameters.slice(1).map(param => { + const paramName = param.name; + const paramValue = parseTopLevelType( + param.typeAnnotation.typeAnnotation, + types, + ).type; + + const type = + paramValue.type === 'TSTypeReference' + ? paramValue.typeName.name + : paramValue.type; + let returnType; + + switch (type) { + case 'RootTag': + returnType = { + type: 'ReservedTypeAnnotation', + name: 'RootTag', + }; + break; + case 'TSBooleanKeyword': + case 'Int32': + case 'Double': + case 'Float': + case 'TSStringKeyword': + returnType = getPrimitiveTypeAnnotation(type); + break; + case 'Array': + case 'ReadOnlyArray': + if (!paramValue.type === 'TSTypeReference') { + throw new Error( + 'Array and ReadOnlyArray are TSTypeReference for array', + ); + } + returnType = { + type: 'ArrayTypeAnnotation', + elementType: getPrimitiveTypeAnnotation( + // TODO: T172453752 support complex type annotation for array element + paramValue.typeParameters.params[0].type, + ), + }; + break; + case 'TSArrayType': + returnType = { + type: 'ArrayTypeAnnotation', + elementType: { + // TODO: T172453752 support complex type annotation for array element + type: getPrimitiveTypeAnnotation(paramValue.elementType.type).type, + }, + }; + break; + default: + (type: empty); + throw new Error( + `Unsupported param type for method "${name}", param "${paramName}". Found ${type}`, + ); + } + + return { + name: paramName, + optional: false, + typeAnnotation: returnType, + }; + }); + + return { + name, + optional, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + params, + returnTypeAnnotation: { + type: 'VoidTypeAnnotation', + }, + }, + }; +} + +function buildCommandSchema( + property: EventTypeAST, + types: TypeDeclarationMap, +): NamedShape { + if (property.type === 'TSPropertySignature') { + const topLevelType = parseTopLevelType( + property.typeAnnotation.typeAnnotation, + types, + ); + const name = property.key.name; + const optional = property.optional || topLevelType.optional; + const parameters = topLevelType.type.parameters || topLevelType.type.params; + return buildCommandSchemaInternal(name, optional, parameters, types); + } else { + const name = property.key.name; + const optional = property.optional || false; + const parameters = property.parameters || property.params; + return buildCommandSchemaInternal(name, optional, parameters, types); + } +} + +function getCommands( + commandTypeAST: $ReadOnlyArray, + types: TypeDeclarationMap, +): $ReadOnlyArray> { + return commandTypeAST + .filter( + property => + property.type === 'TSPropertySignature' || + property.type === 'TSMethodSignature', + ) + .map(property => buildCommandSchema(property, types)) + .filter(Boolean); +} + +module.exports = { + getCommands, +}; diff --git a/packages/react-native-codegen/lib/parsers/typescript/components/componentsUtils.js b/packages/react-native-codegen/lib/parsers/typescript/components/componentsUtils.js new file mode 100644 index 00000000000000..3ba47fe367a879 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/typescript/components/componentsUtils.js @@ -0,0 +1,489 @@ +/** + * 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 _require = require('../../parsers-commons'), + verifyPropNotAlreadyDefined = _require.verifyPropNotAlreadyDefined; +const _require2 = require('../parseTopLevelType'), + flattenIntersectionType = _require2.flattenIntersectionType, + parseTopLevelType = _require2.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 and ReadonlyArray + 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, 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, _extractedTypeAnnotat2; + // unpack WithDefault, (T) or T|U + const topLevelType = parseTopLevelType(typeAnnotation, types); + if (topLevelType.defaultValue !== undefined) { + throw new Error( + 'Nested optionals such as "ReadonlyArray" are not supported, please declare optionals at the top level of value definitions as in "ReadonlyArray | null | undefined"', + ); + } + if (topLevelType.optional) { + throw new Error( + 'Nested optionals such as "ReadonlyArray" are not supported, please declare optionals at the top level of value definitions as in "ReadonlyArray | 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' + ? extractedTypeAnnotation.elementType.typeName.name + : ((_extractedTypeAnnotat = extractedTypeAnnotation.elementType) === + null || _extractedTypeAnnotat === void 0 + ? void 0 + : _extractedTypeAnnotat.type) || + ((_extractedTypeAnnotat2 = extractedTypeAnnotation.typeName) === null || + _extractedTypeAnnotat2 === void 0 + ? void 0 + : _extractedTypeAnnotat2.name) || + 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, 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) { + // unpack WithDefault, (T) or T|U + const topLevelType = parseTopLevelType( + property.typeAnnotation.typeAnnotation, + 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, +}; diff --git a/packages/react-native-codegen/lib/parsers/typescript/components/componentsUtils.js.flow b/packages/react-native-codegen/lib/parsers/typescript/components/componentsUtils.js.flow new file mode 100644 index 00000000000000..f6eb2f12042d2b --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/typescript/components/componentsUtils.js.flow @@ -0,0 +1,533 @@ +/** + * 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 {BuildSchemaFN, Parser} from '../../parser'; +import type {ASTNode, PropAST, TypeDeclarationMap} from '../../utils'; + +const {verifyPropNotAlreadyDefined} = require('../../parsers-commons'); +const { + flattenIntersectionType, + parseTopLevelType, +} = require('../parseTopLevelType'); + +function getUnionOfLiterals( + name: string, + forArray: boolean, + elementTypes: $FlowFixMe[], + defaultValue: $FlowFixMe | void, + types: TypeDeclarationMap, +) { + 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].literal?.type === 'StringLiteral' + ) { + return { + type: 'StringEnumTypeAnnotation', + default: (defaultValue: string), + options: elementTypes.map(option => option.literal.value), + }; + } else if ( + unionType === 'TSLiteralType' && + elementTypes[0].literal?.type === 'NumericLiteral' + ) { + if (forArray) { + throw new Error(`Arrays of int enums are not supported (see: "${name}")`); + } else { + return { + type: 'Int32EnumTypeAnnotation', + default: (defaultValue: number), + options: elementTypes.map(option => option.literal.value), + }; + } + } else { + throw new Error( + `Unsupported union type for "${name}", received "${ + unionType === 'TSLiteralType' + ? elementTypes[0].literal?.type + : unionType + }"`, + ); + } +} + +function detectArrayType( + name: string, + typeAnnotation: $FlowFixMe | ASTNode, + defaultValue: $FlowFixMe | void, + types: TypeDeclarationMap, + parser: Parser, + buildSchema: BuildSchemaFN, +): $FlowFixMe { + // 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 and ReadonlyArray + 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: Array<$FlowFixMe>, + types: TypeDeclarationMap, + parser: Parser, + buildSchema: BuildSchemaFN, +): $FlowFixMe { + const flattenedProperties = flattenProperties(rawProperties, types, parser); + const properties = flattenedProperties + .map(prop => buildSchema(prop, types, parser)) + .filter(Boolean); + + return { + type: 'ObjectTypeAnnotation', + properties, + }; +} + +function getPrimitiveTypeAnnotation(type: string): $FlowFixMe { + 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: string, + forArray: boolean, + type: string, + typeAnnotation: $FlowFixMe, + defaultValue: $FlowFixMe | void, + types: TypeDeclarationMap, + parser: Parser, + buildSchema: BuildSchemaFN, +): $FlowFixMe { + 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, 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: string, + typeAnnotation: $FlowFixMe, + defaultValue: $FlowFixMe | void, + types: TypeDeclarationMap, + parser: Parser, + buildSchema: BuildSchemaFN, +): $FlowFixMe { + // unpack WithDefault, (T) or T|U + const topLevelType = parseTopLevelType(typeAnnotation, types); + if (topLevelType.defaultValue !== undefined) { + throw new Error( + 'Nested optionals such as "ReadonlyArray" are not supported, please declare optionals at the top level of value definitions as in "ReadonlyArray | null | undefined"', + ); + } + if (topLevelType.optional) { + throw new Error( + 'Nested optionals such as "ReadonlyArray" are not supported, please declare optionals at the top level of value definitions as in "ReadonlyArray | 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' + ? extractedTypeAnnotation.elementType.typeName.name + : extractedTypeAnnotation.elementType?.type || + extractedTypeAnnotation.typeName?.name || + 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: empty); + throw new Error(`Unknown prop type for "${name}": ${type}`); + } +} + +function setDefaultValue( + common: $FlowFixMe, + defaultValue: $FlowFixMe | void, +): void { + switch (common.type) { + case 'Int32TypeAnnotation': + case 'DoubleTypeAnnotation': + common.default = ((defaultValue ? defaultValue : 0): number); + break; + case 'FloatTypeAnnotation': + common.default = ((defaultValue === null + ? null + : defaultValue + ? defaultValue + : 0): number | null); + break; + case 'BooleanTypeAnnotation': + common.default = defaultValue === null ? null : !!defaultValue; + break; + case 'StringTypeAnnotation': + common.default = ((defaultValue === undefined ? null : defaultValue): + | string + | null); + break; + } +} + +function getTypeAnnotation( + name: string, + annotation: $FlowFixMe | ASTNode, + defaultValue: $FlowFixMe | void, + withNullDefault: boolean, // Just to make `getTypeAnnotation` signature match with the one from Flow + types: TypeDeclarationMap, + parser: Parser, + buildSchema: BuildSchemaFN, +): $FlowFixMe { + // unpack WithDefault, (T) or T|U + const topLevelType = parseTopLevelType(annotation, 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}"`); + } +} + +type SchemaInfo = { + name: string, + optional: boolean, + typeAnnotation: $FlowFixMe, + defaultValue: $FlowFixMe, + withNullDefault: boolean, // Just to make `getTypeAnnotation` signature match with the one from Flow +}; + +function getSchemaInfo( + property: PropAST, + types: TypeDeclarationMap, +): SchemaInfo { + // unpack WithDefault, (T) or T|U + const topLevelType = parseTopLevelType( + property.typeAnnotation.typeAnnotation, + 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: $ReadOnlyArray, + types: TypeDeclarationMap, + parser: Parser, +): $ReadOnlyArray { + 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: Array, item) => { + if (Array.isArray(item)) { + item.forEach((prop: PropAST) => { + verifyPropNotAlreadyDefined(acc, prop); + }); + return acc.concat(item); + } else { + verifyPropNotAlreadyDefined(acc, item); + acc.push(item); + return acc; + } + }, []) + .filter(Boolean); +} + +module.exports = { + getSchemaInfo, + getTypeAnnotation, + getPrimitiveTypeAnnotation, + flattenProperties, +}; diff --git a/packages/react-native-codegen/lib/parsers/typescript/components/events.js b/packages/react-native-codegen/lib/parsers/typescript/components/events.js new file mode 100644 index 00000000000000..216752c45bd8d5 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/typescript/components/events.js @@ -0,0 +1,261 @@ +/** + * 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 _require = require('../../error-utils'), + throwIfArgumentPropsAreNull = _require.throwIfArgumentPropsAreNull, + throwIfBubblingTypeIsNull = _require.throwIfBubblingTypeIsNull, + throwIfEventHasNoName = _require.throwIfEventHasNoName; +const _require2 = require('../../parsers-commons'), + buildPropertiesForEvent = _require2.buildPropertiesForEvent, + emitBuildEventSchema = _require2.emitBuildEventSchema, + getEventArgument = _require2.getEventArgument, + handleEventHandler = _require2.handleEventHandler; +const _require3 = require('../../parsers-primitives'), + emitBoolProp = _require3.emitBoolProp, + emitDoubleProp = _require3.emitDoubleProp, + emitFloatProp = _require3.emitFloatProp, + emitInt32Prop = _require3.emitInt32Prop, + emitMixedProp = _require3.emitMixedProp, + emitObjectProp = _require3.emitObjectProp, + emitStringProp = _require3.emitStringProp, + emitUnionProp = _require3.emitUnionProp; +const _require4 = require('../parseTopLevelType'), + parseTopLevelType = _require4.parseTopLevelType; +const _require5 = require('./componentsUtils'), + flattenProperties = _require5.flattenProperties; +function getPropertyType( + /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's + * LTI update could not be added via codemod */ + name, + optionalProperty, + /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's + * LTI update could not be added via codemod */ + annotation, + parser, +) { + const topLevelType = parseTopLevelType(annotation); + const typeAnnotation = topLevelType.type; + const optional = optionalProperty || topLevelType.optional; + const type = + typeAnnotation.type === 'TSTypeReference' + ? parser.getTypeAnnotationName(typeAnnotation) + : typeAnnotation.type; + switch (type) { + case 'TSBooleanKeyword': + return emitBoolProp(name, optional); + case 'TSStringKeyword': + return emitStringProp(name, optional); + case 'Int32': + return emitInt32Prop(name, optional); + case 'Double': + return emitDoubleProp(name, optional); + case 'Float': + return emitFloatProp(name, optional); + case 'TSTypeLiteral': + return emitObjectProp( + name, + optional, + parser, + typeAnnotation, + extractArrayElementType, + ); + case 'TSUnionType': + return emitUnionProp(name, optional, parser, typeAnnotation); + case 'UnsafeMixed': + return emitMixedProp(name, optional); + case 'TSArrayType': + return { + name, + optional, + typeAnnotation: extractArrayElementType(typeAnnotation, name, parser), + }; + default: + throw new Error(`Unable to determine event type for "${name}": ${type}`); + } +} +function extractArrayElementType(typeAnnotation, name, parser) { + const type = extractTypeFromTypeAnnotation(typeAnnotation, parser); + switch (type) { + case 'TSParenthesizedType': + return extractArrayElementType( + typeAnnotation.typeAnnotation, + name, + parser, + ); + case 'TSBooleanKeyword': + return { + type: 'BooleanTypeAnnotation', + }; + case 'TSStringKeyword': + return { + type: 'StringTypeAnnotation', + }; + case 'Float': + return { + type: 'FloatTypeAnnotation', + }; + case 'Int32': + return { + type: 'Int32TypeAnnotation', + }; + case 'TSNumberKeyword': + case 'Double': + return { + type: 'DoubleTypeAnnotation', + }; + case 'TSUnionType': + return { + type: 'StringEnumTypeAnnotation', + options: typeAnnotation.types.map(option => + parser.getLiteralValue(option), + ), + }; + case 'TSTypeLiteral': + return { + type: 'ObjectTypeAnnotation', + properties: parser + .getObjectProperties(typeAnnotation) + .map(member => + buildPropertiesForEvent(member, parser, getPropertyType), + ), + }; + case 'TSArrayType': + return { + type: 'ArrayTypeAnnotation', + elementType: extractArrayElementType( + typeAnnotation.elementType, + name, + parser, + ), + }; + default: + throw new Error( + `Unrecognized ${type} for Array ${name} in events.\n${JSON.stringify( + typeAnnotation, + null, + 2, + )}`, + ); + } +} +function extractTypeFromTypeAnnotation(typeAnnotation, parser) { + return typeAnnotation.type === 'TSTypeReference' + ? parser.getTypeAnnotationName(typeAnnotation) + : typeAnnotation.type; +} +function findEventArgumentsAndType( + parser, + typeAnnotation, + types, + bubblingType, + paperName, +) { + if (typeAnnotation.type === 'TSInterfaceDeclaration') { + return { + argumentProps: flattenProperties([typeAnnotation], types, parser), + paperTopLevelNameDeprecated: paperName, + bubblingType, + }; + } + if (typeAnnotation.type === 'TSTypeLiteral') { + return { + argumentProps: parser.getObjectProperties(typeAnnotation), + paperTopLevelNameDeprecated: paperName, + bubblingType, + }; + } + throwIfEventHasNoName(typeAnnotation, parser); + const name = parser.getTypeAnnotationName(typeAnnotation); + if (name === 'Readonly') { + return findEventArgumentsAndType( + parser, + typeAnnotation.typeParameters.params[0], + types, + bubblingType, + paperName, + ); + } else if (name === 'BubblingEventHandler' || name === 'DirectEventHandler') { + return handleEventHandler( + name, + typeAnnotation, + parser, + types, + findEventArgumentsAndType, + ); + } else if (types[name]) { + let elementType = types[name]; + if (elementType.type === 'TSTypeAliasDeclaration') { + elementType = elementType.typeAnnotation; + } + return findEventArgumentsAndType( + parser, + elementType, + types, + bubblingType, + paperName, + ); + } else { + return { + argumentProps: null, + bubblingType: null, + paperTopLevelNameDeprecated: null, + }; + } +} + +// $FlowFixMe[unclear-type] TODO(T108222691): Use flow-types for @babel/parser + +function buildEventSchema(types, property, parser) { + // unpack WithDefault, (T) or T|U + const topLevelType = parseTopLevelType( + property.typeAnnotation.typeAnnotation, + types, + ); + const name = property.key.name; + const typeAnnotation = topLevelType.type; + const optional = property.optional || topLevelType.optional; + const _findEventArgumentsAn = findEventArgumentsAndType( + parser, + typeAnnotation, + types, + ), + argumentProps = _findEventArgumentsAn.argumentProps, + bubblingType = _findEventArgumentsAn.bubblingType, + paperTopLevelNameDeprecated = + _findEventArgumentsAn.paperTopLevelNameDeprecated; + const nonNullableArgumentProps = throwIfArgumentPropsAreNull( + argumentProps, + name, + ); + const nonNullableBubblingType = throwIfBubblingTypeIsNull(bubblingType, name); + const argument = getEventArgument( + nonNullableArgumentProps, + parser, + getPropertyType, + ); + return emitBuildEventSchema( + paperTopLevelNameDeprecated, + name, + optional, + nonNullableBubblingType, + argument, + ); +} +function getEvents(eventTypeAST, types, parser) { + return eventTypeAST + .map(property => buildEventSchema(types, property, parser)) + .filter(Boolean); +} +module.exports = { + getEvents, + extractArrayElementType, +}; diff --git a/packages/react-native-codegen/lib/parsers/typescript/components/events.js.flow b/packages/react-native-codegen/lib/parsers/typescript/components/events.js.flow new file mode 100644 index 00000000000000..a0672ac3570448 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/typescript/components/events.js.flow @@ -0,0 +1,292 @@ +/** + * 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 { + EventTypeAnnotation, + EventTypeShape, + NamedShape, +} from '../../../CodegenSchema.js'; +import type {Parser} from '../../parser'; +import type {TypeDeclarationMap} from '../../utils'; + +const { + throwIfArgumentPropsAreNull, + throwIfBubblingTypeIsNull, + throwIfEventHasNoName, +} = require('../../error-utils'); +const { + buildPropertiesForEvent, + emitBuildEventSchema, + getEventArgument, + handleEventHandler, +} = require('../../parsers-commons'); +const { + emitBoolProp, + emitDoubleProp, + emitFloatProp, + emitInt32Prop, + emitMixedProp, + emitObjectProp, + emitStringProp, + emitUnionProp, +} = require('../../parsers-primitives'); +const {parseTopLevelType} = require('../parseTopLevelType'); +const {flattenProperties} = require('./componentsUtils'); +function getPropertyType( + /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's + * LTI update could not be added via codemod */ + name, + optionalProperty: boolean, + /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's + * LTI update could not be added via codemod */ + annotation, + parser: Parser, +): NamedShape { + const topLevelType = parseTopLevelType(annotation); + const typeAnnotation = topLevelType.type; + const optional = optionalProperty || topLevelType.optional; + const type = + typeAnnotation.type === 'TSTypeReference' + ? parser.getTypeAnnotationName(typeAnnotation) + : typeAnnotation.type; + + switch (type) { + case 'TSBooleanKeyword': + return emitBoolProp(name, optional); + case 'TSStringKeyword': + return emitStringProp(name, optional); + case 'Int32': + return emitInt32Prop(name, optional); + case 'Double': + return emitDoubleProp(name, optional); + case 'Float': + return emitFloatProp(name, optional); + case 'TSTypeLiteral': + return emitObjectProp( + name, + optional, + parser, + typeAnnotation, + extractArrayElementType, + ); + case 'TSUnionType': + return emitUnionProp(name, optional, parser, typeAnnotation); + case 'UnsafeMixed': + return emitMixedProp(name, optional); + case 'TSArrayType': + return { + name, + optional, + typeAnnotation: extractArrayElementType(typeAnnotation, name, parser), + }; + default: + throw new Error(`Unable to determine event type for "${name}": ${type}`); + } +} + +function extractArrayElementType( + typeAnnotation: $FlowFixMe, + name: string, + parser: Parser, +): EventTypeAnnotation { + const type = extractTypeFromTypeAnnotation(typeAnnotation, parser); + + switch (type) { + case 'TSParenthesizedType': + return extractArrayElementType( + typeAnnotation.typeAnnotation, + name, + parser, + ); + case 'TSBooleanKeyword': + return {type: 'BooleanTypeAnnotation'}; + case 'TSStringKeyword': + return {type: 'StringTypeAnnotation'}; + case 'Float': + return { + type: 'FloatTypeAnnotation', + }; + case 'Int32': + return { + type: 'Int32TypeAnnotation', + }; + case 'TSNumberKeyword': + case 'Double': + return { + type: 'DoubleTypeAnnotation', + }; + case 'TSUnionType': + return { + type: 'StringEnumTypeAnnotation', + options: typeAnnotation.types.map(option => + parser.getLiteralValue(option), + ), + }; + case 'TSTypeLiteral': + return { + type: 'ObjectTypeAnnotation', + properties: parser + .getObjectProperties(typeAnnotation) + .map(member => + buildPropertiesForEvent(member, parser, getPropertyType), + ), + }; + case 'TSArrayType': + return { + type: 'ArrayTypeAnnotation', + elementType: extractArrayElementType( + typeAnnotation.elementType, + name, + parser, + ), + }; + default: + throw new Error( + `Unrecognized ${type} for Array ${name} in events.\n${JSON.stringify( + typeAnnotation, + null, + 2, + )}`, + ); + } +} + +function extractTypeFromTypeAnnotation( + typeAnnotation: $FlowFixMe, + parser: Parser, +): string { + return typeAnnotation.type === 'TSTypeReference' + ? parser.getTypeAnnotationName(typeAnnotation) + : typeAnnotation.type; +} + +function findEventArgumentsAndType( + parser: Parser, + typeAnnotation: $FlowFixMe, + types: TypeDeclarationMap, + bubblingType: void | 'direct' | 'bubble', + paperName: ?$FlowFixMe, +): { + argumentProps: ?$ReadOnlyArray<$FlowFixMe>, + paperTopLevelNameDeprecated: ?$FlowFixMe, + bubblingType: ?'direct' | 'bubble', +} { + if (typeAnnotation.type === 'TSInterfaceDeclaration') { + return { + argumentProps: flattenProperties([typeAnnotation], types, parser), + paperTopLevelNameDeprecated: paperName, + bubblingType, + }; + } + + if (typeAnnotation.type === 'TSTypeLiteral') { + return { + argumentProps: parser.getObjectProperties(typeAnnotation), + paperTopLevelNameDeprecated: paperName, + bubblingType, + }; + } + + throwIfEventHasNoName(typeAnnotation, parser); + const name = parser.getTypeAnnotationName(typeAnnotation); + if (name === 'Readonly') { + return findEventArgumentsAndType( + parser, + typeAnnotation.typeParameters.params[0], + types, + bubblingType, + paperName, + ); + } else if (name === 'BubblingEventHandler' || name === 'DirectEventHandler') { + return handleEventHandler( + name, + typeAnnotation, + parser, + types, + findEventArgumentsAndType, + ); + } else if (types[name]) { + let elementType = types[name]; + if (elementType.type === 'TSTypeAliasDeclaration') { + elementType = elementType.typeAnnotation; + } + return findEventArgumentsAndType( + parser, + elementType, + types, + bubblingType, + paperName, + ); + } else { + return { + argumentProps: null, + bubblingType: null, + paperTopLevelNameDeprecated: null, + }; + } +} + +// $FlowFixMe[unclear-type] TODO(T108222691): Use flow-types for @babel/parser +type EventTypeAST = Object; + +function buildEventSchema( + types: TypeDeclarationMap, + property: EventTypeAST, + parser: Parser, +): ?EventTypeShape { + // unpack WithDefault, (T) or T|U + const topLevelType = parseTopLevelType( + property.typeAnnotation.typeAnnotation, + types, + ); + + const name = property.key.name; + const typeAnnotation = topLevelType.type; + const optional = property.optional || topLevelType.optional; + const {argumentProps, bubblingType, paperTopLevelNameDeprecated} = + findEventArgumentsAndType(parser, typeAnnotation, types); + + const nonNullableArgumentProps = throwIfArgumentPropsAreNull( + argumentProps, + name, + ); + const nonNullableBubblingType = throwIfBubblingTypeIsNull(bubblingType, name); + + const argument = getEventArgument( + nonNullableArgumentProps, + parser, + getPropertyType, + ); + + return emitBuildEventSchema( + paperTopLevelNameDeprecated, + name, + optional, + nonNullableBubblingType, + argument, + ); +} + +function getEvents( + eventTypeAST: $ReadOnlyArray, + types: TypeDeclarationMap, + parser: Parser, +): $ReadOnlyArray { + return eventTypeAST + .map(property => buildEventSchema(types, property, parser)) + .filter(Boolean); +} + +module.exports = { + getEvents, + extractArrayElementType, +}; diff --git a/packages/react-native-codegen/lib/parsers/typescript/components/extends.js b/packages/react-native-codegen/lib/parsers/typescript/components/extends.js new file mode 100644 index 00000000000000..4c1f72301273c4 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/typescript/components/extends.js @@ -0,0 +1,41 @@ +/** + * 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 _require = require('../parseTopLevelType'), + parseTopLevelType = _require.parseTopLevelType; +function isEvent(typeAnnotation) { + if (typeAnnotation.type !== 'TSTypeReference') { + return false; + } + const eventNames = new Set(['BubblingEventHandler', 'DirectEventHandler']); + return eventNames.has(typeAnnotation.typeName.name); +} + +// $FlowFixMe[unclear-type] TODO(T108222691): Use flow-types for @babel/parser + +function categorizeProps(typeDefinition, types, events) { + // find events + for (const prop of typeDefinition) { + if (prop.type === 'TSPropertySignature') { + const topLevelType = parseTopLevelType( + prop.typeAnnotation.typeAnnotation, + types, + ); + if (isEvent(topLevelType.type)) { + events.push(prop); + } + } + } +} +module.exports = { + categorizeProps, +}; diff --git a/packages/react-native-codegen/lib/parsers/typescript/components/extends.js.flow b/packages/react-native-codegen/lib/parsers/typescript/components/extends.js.flow new file mode 100644 index 00000000000000..6e29af9caf1379 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/typescript/components/extends.js.flow @@ -0,0 +1,50 @@ +/** + * 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 {TypeDeclarationMap} from '../../utils'; + +const {parseTopLevelType} = require('../parseTopLevelType'); + +function isEvent(typeAnnotation: $FlowFixMe): boolean { + if (typeAnnotation.type !== 'TSTypeReference') { + return false; + } + const eventNames = new Set(['BubblingEventHandler', 'DirectEventHandler']); + return eventNames.has(typeAnnotation.typeName.name); +} + +// $FlowFixMe[unclear-type] TODO(T108222691): Use flow-types for @babel/parser +type PropsAST = Object; + +function categorizeProps( + typeDefinition: $ReadOnlyArray, + types: TypeDeclarationMap, + events: Array, +): void { + // find events + for (const prop of typeDefinition) { + if (prop.type === 'TSPropertySignature') { + const topLevelType = parseTopLevelType( + prop.typeAnnotation.typeAnnotation, + types, + ); + + if (isEvent(topLevelType.type)) { + events.push(prop); + } + } + } +} + +module.exports = { + categorizeProps, +}; diff --git a/packages/react-native-codegen/lib/parsers/typescript/components/index.js b/packages/react-native-codegen/lib/parsers/typescript/components/index.js new file mode 100644 index 00000000000000..c7ca2e72963511 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/typescript/components/index.js @@ -0,0 +1,55 @@ +/** + * 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 _require = require('../../parsers-commons'), + findComponentConfig = _require.findComponentConfig, + getCommandProperties = _require.getCommandProperties, + getOptions = _require.getOptions; +const _require2 = require('./commands'), + getCommands = _require2.getCommands; +const _require3 = require('./events'), + getEvents = _require3.getEvents; +const _require4 = require('./extends'), + categorizeProps = _require4.categorizeProps; + +// $FlowFixMe[unclear-type] TODO(T108222691): Use flow-types for @babel/parser + +// $FlowFixMe[signature-verification-failure] TODO(T108222691): Use flow-types for @babel/parser +function buildComponentSchema(ast, parser) { + const _findComponentConfig = findComponentConfig(ast, parser), + componentName = _findComponentConfig.componentName, + propsTypeName = _findComponentConfig.propsTypeName, + optionsExpression = _findComponentConfig.optionsExpression; + const types = parser.getTypes(ast); + const propProperties = parser.getProperties(propsTypeName, types); + const commandProperties = getCommandProperties(ast, parser); + const options = getOptions(optionsExpression); + const componentEventAsts = []; + categorizeProps(propProperties, types, componentEventAsts); + const _parser$getProps = parser.getProps(propProperties, types), + props = _parser$getProps.props, + extendsProps = _parser$getProps.extendsProps; + const events = getEvents(componentEventAsts, types, parser); + const commands = getCommands(commandProperties, types); + return { + filename: componentName, + componentName, + options, + extendsProps, + events, + props, + commands, + }; +} +module.exports = { + buildComponentSchema, +}; diff --git a/packages/react-native-codegen/lib/parsers/typescript/components/index.js.flow b/packages/react-native-codegen/lib/parsers/typescript/components/index.js.flow new file mode 100644 index 00000000000000..fa95c8f7e41b8f --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/typescript/components/index.js.flow @@ -0,0 +1,64 @@ +/** + * 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 {Parser} from '../../parser'; +import type {ComponentSchemaBuilderConfig} from '../../schema.js'; + +const { + findComponentConfig, + getCommandProperties, + getOptions, +} = require('../../parsers-commons'); +const {getCommands} = require('./commands'); +const {getEvents} = require('./events'); +const {categorizeProps} = require('./extends'); + +// $FlowFixMe[unclear-type] TODO(T108222691): Use flow-types for @babel/parser +type PropsAST = Object; + +// $FlowFixMe[signature-verification-failure] TODO(T108222691): Use flow-types for @babel/parser +function buildComponentSchema( + ast: $FlowFixMe, + parser: Parser, +): ComponentSchemaBuilderConfig { + const {componentName, propsTypeName, optionsExpression} = findComponentConfig( + ast, + parser, + ); + + const types = parser.getTypes(ast); + + const propProperties = parser.getProperties(propsTypeName, types); + + const commandProperties = getCommandProperties(ast, parser); + + const options = getOptions(optionsExpression); + + const componentEventAsts: Array = []; + categorizeProps(propProperties, types, componentEventAsts); + const {props, extendsProps} = parser.getProps(propProperties, types); + const events = getEvents(componentEventAsts, types, parser); + const commands = getCommands(commandProperties, types); + + return { + filename: componentName, + componentName, + options, + extendsProps, + events, + props, + commands, + }; +} + +module.exports = { + buildComponentSchema, +}; diff --git a/packages/react-native-codegen/lib/parsers/typescript/modules/__test_fixtures__/failures.js b/packages/react-native-codegen/lib/parsers/typescript/modules/__test_fixtures__/failures.js new file mode 100644 index 00000000000000..59f52acef62c05 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/typescript/modules/__test_fixtures__/failures.js @@ -0,0 +1,212 @@ +/** + * 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 + */ + +// @licenselint-loose-mode + +const NATIVE_MODULES_WITH_ARRAY_WITH_NO_TYPE_FOR_CONTENT = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + getString: (arg: string) => Array; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; +const NATIVE_MODULES_WITH_ARRAY_WITH_NO_TYPE_FOR_CONTENT_AS_PARAM = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + getString: (arg: Array) => string; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; +const NATIVE_MODULES_WITH_NOT_ONLY_METHODS = ` +/** + * 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 + */ + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + readonly getBool: (arg: boolean) => boolean; + readonly getNumber: (arg: number) => number; + readonly getString: (arg: string) => string; + sampleBool: boolean, + +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; +const NATIVE_MODULES_WITH_UNNAMED_PARAMS = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + readonly getBool: (boolean) => boolean; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; +const NATIVE_MODULES_WITH_PROMISE_WITHOUT_TYPE = ` +/** + * 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 + */ + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + readonly getBool: (arg: boolean) => Promise; + +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; +const TWO_NATIVE_MODULES_EXPORTED_WITH_DEFAULT = ` +/** + * 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 + */ + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule1'); +export default TurboModuleRegistry.getEnforcing('SampleTurboModule2'); +`; +const TWO_NATIVE_EXTENDING_TURBO_MODULE = ` +/** + * 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 + */ + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + readonly getSth: (a: number | null | undefined) => void; +} + +export interface Spec2 extends TurboModule { + readonly getSth: (a: number | null | undefined) => void; +} +`; +const EMPTY_ENUM_NATIVE_MODULE = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export enum SomeEnum { +} + +export interface Spec extends TurboModule { + readonly getEnums: (a: SomeEnum) => string; +} + +export default TurboModuleRegistry.getEnforcing( + 'EmptyEnumNativeModule', +); +`; +const MIXED_VALUES_ENUM_NATIVE_MODULE = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export enum SomeEnum { + NUM = 1, + STR = 'str', +} + +export interface Spec extends TurboModule { + readonly getEnums: (a: SomeEnum) => string; +} + +export default TurboModuleRegistry.getEnforcing( + 'MixedValuesEnumNativeModule', +); +`; +module.exports = { + NATIVE_MODULES_WITH_UNNAMED_PARAMS, + NATIVE_MODULES_WITH_PROMISE_WITHOUT_TYPE, + NATIVE_MODULES_WITH_ARRAY_WITH_NO_TYPE_FOR_CONTENT_AS_PARAM, + NATIVE_MODULES_WITH_ARRAY_WITH_NO_TYPE_FOR_CONTENT, + TWO_NATIVE_MODULES_EXPORTED_WITH_DEFAULT, + NATIVE_MODULES_WITH_NOT_ONLY_METHODS, + TWO_NATIVE_EXTENDING_TURBO_MODULE, + EMPTY_ENUM_NATIVE_MODULE, + MIXED_VALUES_ENUM_NATIVE_MODULE, +}; diff --git a/packages/react-native-codegen/lib/parsers/typescript/modules/__test_fixtures__/failures.js.flow b/packages/react-native-codegen/lib/parsers/typescript/modules/__test_fixtures__/failures.js.flow new file mode 100644 index 00000000000000..a8d08e8395ff9a --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/typescript/modules/__test_fixtures__/failures.js.flow @@ -0,0 +1,221 @@ +/** + * 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-local + * @format + */ + +// @licenselint-loose-mode + +const NATIVE_MODULES_WITH_ARRAY_WITH_NO_TYPE_FOR_CONTENT = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + getString: (arg: string) => Array; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; + +const NATIVE_MODULES_WITH_ARRAY_WITH_NO_TYPE_FOR_CONTENT_AS_PARAM = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + getString: (arg: Array) => string; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; + +const NATIVE_MODULES_WITH_NOT_ONLY_METHODS = ` +/** + * 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 + */ + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + readonly getBool: (arg: boolean) => boolean; + readonly getNumber: (arg: number) => number; + readonly getString: (arg: string) => string; + sampleBool: boolean, + +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULES_WITH_UNNAMED_PARAMS = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + readonly getBool: (boolean) => boolean; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; + +const NATIVE_MODULES_WITH_PROMISE_WITHOUT_TYPE = ` +/** + * 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 + */ + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + readonly getBool: (arg: boolean) => Promise; + +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const TWO_NATIVE_MODULES_EXPORTED_WITH_DEFAULT = ` +/** + * 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 + */ + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule1'); +export default TurboModuleRegistry.getEnforcing('SampleTurboModule2'); +`; + +const TWO_NATIVE_EXTENDING_TURBO_MODULE = ` +/** + * 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 + */ + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + readonly getSth: (a: number | null | undefined) => void; +} + +export interface Spec2 extends TurboModule { + readonly getSth: (a: number | null | undefined) => void; +} +`; + +const EMPTY_ENUM_NATIVE_MODULE = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export enum SomeEnum { +} + +export interface Spec extends TurboModule { + readonly getEnums: (a: SomeEnum) => string; +} + +export default TurboModuleRegistry.getEnforcing( + 'EmptyEnumNativeModule', +); +`; + +const MIXED_VALUES_ENUM_NATIVE_MODULE = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export enum SomeEnum { + NUM = 1, + STR = 'str', +} + +export interface Spec extends TurboModule { + readonly getEnums: (a: SomeEnum) => string; +} + +export default TurboModuleRegistry.getEnforcing( + 'MixedValuesEnumNativeModule', +); +`; + +module.exports = { + NATIVE_MODULES_WITH_UNNAMED_PARAMS, + NATIVE_MODULES_WITH_PROMISE_WITHOUT_TYPE, + NATIVE_MODULES_WITH_ARRAY_WITH_NO_TYPE_FOR_CONTENT_AS_PARAM, + NATIVE_MODULES_WITH_ARRAY_WITH_NO_TYPE_FOR_CONTENT, + TWO_NATIVE_MODULES_EXPORTED_WITH_DEFAULT, + NATIVE_MODULES_WITH_NOT_ONLY_METHODS, + TWO_NATIVE_EXTENDING_TURBO_MODULE, + EMPTY_ENUM_NATIVE_MODULE, + MIXED_VALUES_ENUM_NATIVE_MODULE, +}; diff --git a/packages/react-native-codegen/lib/parsers/typescript/modules/__test_fixtures__/fixtures.js b/packages/react-native-codegen/lib/parsers/typescript/modules/__test_fixtures__/fixtures.js new file mode 100644 index 00000000000000..e2676ea7028453 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/typescript/modules/__test_fixtures__/fixtures.js @@ -0,0 +1,852 @@ +/** + * 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 + */ + +// @licenselint-loose-mode + +const EMPTY_NATIVE_MODULE = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; +const NATIVE_MODULE_WITH_COMPLEX_OBJECTS = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export type String = string; + +export interface Spec extends TurboModule { + // Exported methods. + readonly getObject: (arg: {const1: {const1: boolean}}) => { + const1: {const1: boolean}, + }; + readonly getReadOnlyObject: (arg: Readonly<{const1: Readonly<{const1: boolean}>}>) => Readonly<{ + const1: {const1: boolean}, + }>; + readonly getObject2: (arg: { a: String }) => Object; + readonly getObjectInArray: (arg: {const1: {const1: boolean}}) => Array<{ + const1: {const1: boolean}, + }>; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; +const NATIVE_MODULE_WITH_COMPLEX_OBJECTS_WITH_NULLABLE_KEY = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export type String = string; + +export interface Spec extends TurboModule { + readonly getConstants: () => { + isTesting: boolean; + reactNativeVersion: { + major: number; + minor: number; + patch?: number; + prerelease: number | null | undefined; + }; + forceTouchAvailable: boolean; + osVersion: string; + systemName: string; + interfaceIdiom: string; + }; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; +const NATIVE_MODULE_WITH_BASIC_PARAM_TYPES = ` +/** + * 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 + */ + + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + readonly passBool?: (arg: boolean) => void; + readonly passNumber: (arg: number) => void; + readonly passString: (arg: string) => void; + readonly passStringish: (arg: Stringish) => void; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; +const NATIVE_MODULE_WITH_ALIASES = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +type NumNum = number; +export type Num = (arg: NumNum) => void; +type Num2 = Num; +export type Void = void; +export type A = number; +export type B = number; +export type ObjectAlias = { + x: number; + y: number; + label: string; + truthy: boolean; +}; +export type ReadOnlyAlias = Readonly; + +export interface Spec extends TurboModule { + // Exported methods. + readonly getNumber: Num2; + readonly getVoid: () => Void; + readonly getArray: (a: Array) => {a: B}; + readonly getStringFromAlias: (a: ObjectAlias) => string; + readonly getStringFromNullableAlias: (a: ObjectAlias | null) => string; + readonly getStringFromReadOnlyAlias: (a: ReadOnlyAlias) => string; + readonly getStringFromNullableReadOnlyAlias: (a: ReadOnlyAlias | null) => string; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; +const NATIVE_MODULE_WITH_NESTED_ALIASES = ` +/** + * 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 + */ + + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +type Bar = { + z: number +}; + +type Foo = { + bar1: Bar, + bar2: Bar, +}; + +export interface Spec extends TurboModule { + // Exported methods. + foo1: (x: Foo) => Foo; + foo2: (x: Foo) => void; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; +const NATIVE_MODULE_WITH_NESTED_INTERFACES = ` +/** + * 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 + */ + + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +interface Bar { + z: number +}; + +interface Base1 { + bar1: Bar, +} + +interface Base2 { + bar2: Bar, +} + +interface Base3 extends Base2 { + bar3: Bar, +} + +interface Foo extends Base1, Base3 { + bar4: Bar, +}; + +export interface Spec extends TurboModule { + // Exported methods. + foo1: (x: Foo) => Foo; + foo2: (x: Foo) => void; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; +const NATIVE_MODULE_WITH_INTERSECTION_TYPES = ` +/** + * 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 + */ + + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +type Bar = { + z: number +}; + +type Base1 = { + bar1: Bar, +} + +type Base2 = { + bar2: Bar, +} + +type Base3 = Base2 & { + bar3: Bar, +} + +type Foo = Base1 & Base3 & { + bar4: Bar, +}; + +export interface Spec extends TurboModule { + // Exported methods. + foo1: (x: Foo) => Foo; + foo2: (x: Foo) => void; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; +const NATIVE_MODULE_WITH_FLOAT_AND_INT32 = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import type {Int32, Float} from 'react-native/Libraries/Types/CodegenTypes'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + readonly getInt: (arg: Int32) => Int32; + readonly getFloat: (arg: Float) => Float; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; +const NATIVE_MODULE_WITH_SIMPLE_OBJECT = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + readonly getObject: (o: Object) => Object, +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; +const NATIVE_MODULE_WITH_UNSAFE_OBJECT = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; +import type {UnsafeObject} from 'react-native/Libraries/Types/CodegenTypes'; + +export interface Spec extends TurboModule { + readonly getUnsafeObject: (o: UnsafeObject) => UnsafeObject; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; +const NATIVE_MODULE_WITH_PARTIALS = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export type SomeObj = { + a: string, + b?: boolean, +}; + +export interface Spec extends TurboModule { + getSomeObj: () => SomeObj; + getPartialSomeObj: () => Partial; + getSomeObjFromPartialSomeObj: (value: Partial) => SomeObj; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; +const NATIVE_MODULE_WITH_PARTIALS_COMPLEX = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export type SomeObj = { + a: string, + b?: boolean, +}; + +export type PartialSomeObj = Partial; + +export interface Spec extends TurboModule { + getPartialPartial: (value1: Partial, value2: PartialSomeObj) => SomeObj; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; +const NATIVE_MODULE_WITH_ROOT_TAG = ` +/** + * 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 + */ + +import type { + TurboModule, + RootTag, +} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + readonly getRootTag: (rootTag: RootTag) => RootTag; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; +const NATIVE_MODULE_WITH_NULLABLE_PARAM = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + readonly voidFunc: (arg: string | null | undefined) => void; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; +const NATIVE_MODULE_WITH_BASIC_ARRAY = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + readonly getArray: (arg: Array) => (Array<(string)>); + readonly getArray: (arg: ReadonlyArray) => (ReadonlyArray<(string)>); +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; +const NATIVE_MODULE_WITH_BASIC_ARRAY2 = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + readonly getArray: (arg: string[]) => ((string)[]); + readonly getArray: (arg: readonly string[]) => (readonly (string)[]); +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; +const NATIVE_MODULE_WITH_OBJECT_WITH_OBJECT_DEFINED_IN_FILE_AS_PROPERTY = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +type DisplayMetricsAndroid = { + width: number; +}; + +export interface Spec extends TurboModule { + readonly getConstants: () => { + readonly Dimensions: { + windowPhysicalPixels: DisplayMetricsAndroid; + }; + }; + readonly getConstants2: () => Readonly<{ + readonly Dimensions: { + windowPhysicalPixels: DisplayMetricsAndroid; + }; + }>; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; +const NATIVE_MODULE_WITH_ARRAY_WITH_UNION_AND_TOUPLE = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + readonly getArray: ( + arg: Array<[string, string]>, + ) => Array; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; +const NATIVE_MODULE_WITH_ARRAY2_WITH_UNION_AND_TOUPLE = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + getArray( + arg: [string, string][], + ): (string | number | boolean)[]; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; +const NATIVE_MODULE_WITH_ARRAY_WITH_ALIAS = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export type SomeString = string; + +export interface Spec extends TurboModule { + readonly getArray: (arg: Array) => Array; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; +const NATIVE_MODULE_WITH_ARRAY2_WITH_ALIAS = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export type SomeString = string; + +export interface Spec extends TurboModule { + readonly getArray: (arg: SomeString[]) => string[]; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; +const NATIVE_MODULE_WITH_COMPLEX_ARRAY = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + readonly getArray: ( + arg: Array>>>>, + ) => Array>>; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; +const NATIVE_MODULE_WITH_COMPLEX_ARRAY2 = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + readonly getArray: ( + arg: string[][][][][], + ) => string[][][]; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; +const NATIVE_MODULE_WITH_PROMISE = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export type String = string; +export type SomeObj = { a: string }; + +export interface Spec extends TurboModule { + readonly getValueWithPromise: () => Promise; + readonly getValueWithPromiseDefinedSomewhereElse: () => Promise; + readonly getValueWithPromiseObjDefinedSomewhereElse: () => Promise; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; +const NATIVE_MODULE_WITH_CALLBACK = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + readonly getValueWithCallback: ( + callback: (value: string, arr: Array>) => void, + ) => void; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; +const NATIVE_MODULE_WITH_UNION = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export type ChooseInt = 1 | 2 | 3; +export type ChooseFloat = 1.44 | 2.88 | 5.76; +export type ChooseObject = {} | {low: string}; +export type ChooseString = 'One' | 'Two' | 'Three'; + +export interface Spec extends TurboModule { + readonly getUnion: (chooseInt: ChooseInt, chooseFloat: ChooseFloat, chooseObject: ChooseObject, chooseString: ChooseString) => ChooseObject; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; +const ANDROID_ONLY_NATIVE_MODULE = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule {} + +export default TurboModuleRegistry.getEnforcing( + 'SampleTurboModuleAndroid', +); +`; +const IOS_ONLY_NATIVE_MODULE = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export enum Quality { + SD, + HD, +} + +export enum Resolution { + Low = 720, + High = 1080, +} + +export enum StringOptions { + One = 'one', + Two = 'two', + Three = 'three', +} + +export interface Spec extends TurboModule { + readonly getEnums: (quality: Quality, resolution?: Resolution, stringOptions: StringOptions) => string; +} + +export default TurboModuleRegistry.getEnforcing( + 'SampleTurboModuleIOS', +); +`; +const CXX_ONLY_NATIVE_MODULE = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export enum Quality { + SD, + HD, +} + +export enum Resolution { + Low = 720, + High = 1080, +} + +export enum StringOptions { + One = 'one', + Two = 'two', + Three = 'three', +} + +export type ChooseInt = 1 | 2 | 3; +export type ChooseFloat = 1.44 | 2.88 | 5.76; +export type ChooseObject = {} | {low: string}; +export type ChooseString = 'One' | 'Two' | 'Three'; + +export type BinaryTreeNode = { + left?: BinaryTreeNode, + value: number, + right?: BinaryTreeNode, +}; + +export type GraphNode = { + label: string, + neighbors?: Array, +}; + +export type CustomDeviceEvent = { + type: string, + level: number, + degree?: number, +}; + +export interface Spec extends TurboModule { + readonly getCallback: () => () => void; + readonly getMixed: (arg: unknown) => unknown; + readonly getEnums: (quality: Quality, resolution?: Resolution, stringOptions: StringOptions) => string; + readonly getBinaryTreeNode: (arg: BinaryTreeNode) => BinaryTreeNode; + readonly getGraphNode: (arg: GraphNode) => GraphNode; + readonly getMap: (arg: {[a: string]: number | null;}) => {[b: string]: number | null;}; + readonly getAnotherMap: (arg: {[key: string]: string}) => {[key: string]: string}; + readonly getUnion: (chooseInt: ChooseInt, chooseFloat: ChooseFloat, chooseObject: ChooseObject, chooseString: ChooseString) => ChooseObject; +} + +export default TurboModuleRegistry.getEnforcing( + 'SampleTurboModuleCxx', +); +`; +module.exports = { + NATIVE_MODULE_WITH_OBJECT_WITH_OBJECT_DEFINED_IN_FILE_AS_PROPERTY, + NATIVE_MODULE_WITH_ARRAY_WITH_UNION_AND_TOUPLE, + NATIVE_MODULE_WITH_ARRAY2_WITH_UNION_AND_TOUPLE, + NATIVE_MODULE_WITH_FLOAT_AND_INT32, + NATIVE_MODULE_WITH_ALIASES, + NATIVE_MODULE_WITH_NESTED_ALIASES, + NATIVE_MODULE_WITH_NESTED_INTERFACES, + NATIVE_MODULE_WITH_INTERSECTION_TYPES, + NATIVE_MODULE_WITH_PROMISE, + NATIVE_MODULE_WITH_COMPLEX_OBJECTS, + NATIVE_MODULE_WITH_COMPLEX_OBJECTS_WITH_NULLABLE_KEY, + NATIVE_MODULE_WITH_SIMPLE_OBJECT, + NATIVE_MODULE_WITH_UNSAFE_OBJECT, + NATIVE_MODULE_WITH_PARTIALS, + NATIVE_MODULE_WITH_PARTIALS_COMPLEX, + NATIVE_MODULE_WITH_ROOT_TAG, + NATIVE_MODULE_WITH_NULLABLE_PARAM, + NATIVE_MODULE_WITH_BASIC_ARRAY, + NATIVE_MODULE_WITH_BASIC_ARRAY2, + NATIVE_MODULE_WITH_COMPLEX_ARRAY, + NATIVE_MODULE_WITH_COMPLEX_ARRAY2, + NATIVE_MODULE_WITH_ARRAY_WITH_ALIAS, + NATIVE_MODULE_WITH_ARRAY2_WITH_ALIAS, + NATIVE_MODULE_WITH_BASIC_PARAM_TYPES, + NATIVE_MODULE_WITH_CALLBACK, + NATIVE_MODULE_WITH_UNION, + EMPTY_NATIVE_MODULE, + ANDROID_ONLY_NATIVE_MODULE, + IOS_ONLY_NATIVE_MODULE, + CXX_ONLY_NATIVE_MODULE, +}; diff --git a/packages/react-native-codegen/lib/parsers/typescript/modules/__test_fixtures__/fixtures.js.flow b/packages/react-native-codegen/lib/parsers/typescript/modules/__test_fixtures__/fixtures.js.flow new file mode 100644 index 00000000000000..453e189144037e --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/typescript/modules/__test_fixtures__/fixtures.js.flow @@ -0,0 +1,882 @@ +/** + * 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-local + * @format + */ + +// @licenselint-loose-mode + +const EMPTY_NATIVE_MODULE = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; + +const NATIVE_MODULE_WITH_COMPLEX_OBJECTS = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export type String = string; + +export interface Spec extends TurboModule { + // Exported methods. + readonly getObject: (arg: {const1: {const1: boolean}}) => { + const1: {const1: boolean}, + }; + readonly getReadOnlyObject: (arg: Readonly<{const1: Readonly<{const1: boolean}>}>) => Readonly<{ + const1: {const1: boolean}, + }>; + readonly getObject2: (arg: { a: String }) => Object; + readonly getObjectInArray: (arg: {const1: {const1: boolean}}) => Array<{ + const1: {const1: boolean}, + }>; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; + +const NATIVE_MODULE_WITH_COMPLEX_OBJECTS_WITH_NULLABLE_KEY = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export type String = string; + +export interface Spec extends TurboModule { + readonly getConstants: () => { + isTesting: boolean; + reactNativeVersion: { + major: number; + minor: number; + patch?: number; + prerelease: number | null | undefined; + }; + forceTouchAvailable: boolean; + osVersion: string; + systemName: string; + interfaceIdiom: string; + }; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; + +const NATIVE_MODULE_WITH_BASIC_PARAM_TYPES = ` +/** + * 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 + */ + + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + readonly passBool?: (arg: boolean) => void; + readonly passNumber: (arg: number) => void; + readonly passString: (arg: string) => void; + readonly passStringish: (arg: Stringish) => void; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULE_WITH_ALIASES = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +type NumNum = number; +export type Num = (arg: NumNum) => void; +type Num2 = Num; +export type Void = void; +export type A = number; +export type B = number; +export type ObjectAlias = { + x: number; + y: number; + label: string; + truthy: boolean; +}; +export type ReadOnlyAlias = Readonly; + +export interface Spec extends TurboModule { + // Exported methods. + readonly getNumber: Num2; + readonly getVoid: () => Void; + readonly getArray: (a: Array) => {a: B}; + readonly getStringFromAlias: (a: ObjectAlias) => string; + readonly getStringFromNullableAlias: (a: ObjectAlias | null) => string; + readonly getStringFromReadOnlyAlias: (a: ReadOnlyAlias) => string; + readonly getStringFromNullableReadOnlyAlias: (a: ReadOnlyAlias | null) => string; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; + +const NATIVE_MODULE_WITH_NESTED_ALIASES = ` +/** + * 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 + */ + + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +type Bar = { + z: number +}; + +type Foo = { + bar1: Bar, + bar2: Bar, +}; + +export interface Spec extends TurboModule { + // Exported methods. + foo1: (x: Foo) => Foo; + foo2: (x: Foo) => void; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULE_WITH_NESTED_INTERFACES = ` +/** + * 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 + */ + + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +interface Bar { + z: number +}; + +interface Base1 { + bar1: Bar, +} + +interface Base2 { + bar2: Bar, +} + +interface Base3 extends Base2 { + bar3: Bar, +} + +interface Foo extends Base1, Base3 { + bar4: Bar, +}; + +export interface Spec extends TurboModule { + // Exported methods. + foo1: (x: Foo) => Foo; + foo2: (x: Foo) => void; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULE_WITH_INTERSECTION_TYPES = ` +/** + * 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 + */ + + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +type Bar = { + z: number +}; + +type Base1 = { + bar1: Bar, +} + +type Base2 = { + bar2: Bar, +} + +type Base3 = Base2 & { + bar3: Bar, +} + +type Foo = Base1 & Base3 & { + bar4: Bar, +}; + +export interface Spec extends TurboModule { + // Exported methods. + foo1: (x: Foo) => Foo; + foo2: (x: Foo) => void; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULE_WITH_FLOAT_AND_INT32 = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import type {Int32, Float} from 'react-native/Libraries/Types/CodegenTypes'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + readonly getInt: (arg: Int32) => Int32; + readonly getFloat: (arg: Float) => Float; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; + +const NATIVE_MODULE_WITH_SIMPLE_OBJECT = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + readonly getObject: (o: Object) => Object, +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; + +const NATIVE_MODULE_WITH_UNSAFE_OBJECT = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; +import type {UnsafeObject} from 'react-native/Libraries/Types/CodegenTypes'; + +export interface Spec extends TurboModule { + readonly getUnsafeObject: (o: UnsafeObject) => UnsafeObject; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; + +const NATIVE_MODULE_WITH_PARTIALS = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export type SomeObj = { + a: string, + b?: boolean, +}; + +export interface Spec extends TurboModule { + getSomeObj: () => SomeObj; + getPartialSomeObj: () => Partial; + getSomeObjFromPartialSomeObj: (value: Partial) => SomeObj; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; + +const NATIVE_MODULE_WITH_PARTIALS_COMPLEX = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export type SomeObj = { + a: string, + b?: boolean, +}; + +export type PartialSomeObj = Partial; + +export interface Spec extends TurboModule { + getPartialPartial: (value1: Partial, value2: PartialSomeObj) => SomeObj; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; + +const NATIVE_MODULE_WITH_ROOT_TAG = ` +/** + * 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 + */ + +import type { + TurboModule, + RootTag, +} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + readonly getRootTag: (rootTag: RootTag) => RootTag; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; + +const NATIVE_MODULE_WITH_NULLABLE_PARAM = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + readonly voidFunc: (arg: string | null | undefined) => void; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; + +const NATIVE_MODULE_WITH_BASIC_ARRAY = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + readonly getArray: (arg: Array) => (Array<(string)>); + readonly getArray: (arg: ReadonlyArray) => (ReadonlyArray<(string)>); +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; + +const NATIVE_MODULE_WITH_BASIC_ARRAY2 = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + readonly getArray: (arg: string[]) => ((string)[]); + readonly getArray: (arg: readonly string[]) => (readonly (string)[]); +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; + +const NATIVE_MODULE_WITH_OBJECT_WITH_OBJECT_DEFINED_IN_FILE_AS_PROPERTY = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +type DisplayMetricsAndroid = { + width: number; +}; + +export interface Spec extends TurboModule { + readonly getConstants: () => { + readonly Dimensions: { + windowPhysicalPixels: DisplayMetricsAndroid; + }; + }; + readonly getConstants2: () => Readonly<{ + readonly Dimensions: { + windowPhysicalPixels: DisplayMetricsAndroid; + }; + }>; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; + +const NATIVE_MODULE_WITH_ARRAY_WITH_UNION_AND_TOUPLE = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + readonly getArray: ( + arg: Array<[string, string]>, + ) => Array; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; + +const NATIVE_MODULE_WITH_ARRAY2_WITH_UNION_AND_TOUPLE = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + getArray( + arg: [string, string][], + ): (string | number | boolean)[]; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; + +const NATIVE_MODULE_WITH_ARRAY_WITH_ALIAS = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export type SomeString = string; + +export interface Spec extends TurboModule { + readonly getArray: (arg: Array) => Array; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; + +const NATIVE_MODULE_WITH_ARRAY2_WITH_ALIAS = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export type SomeString = string; + +export interface Spec extends TurboModule { + readonly getArray: (arg: SomeString[]) => string[]; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; + +const NATIVE_MODULE_WITH_COMPLEX_ARRAY = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + readonly getArray: ( + arg: Array>>>>, + ) => Array>>; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; + +const NATIVE_MODULE_WITH_COMPLEX_ARRAY2 = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + readonly getArray: ( + arg: string[][][][][], + ) => string[][][]; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; + +const NATIVE_MODULE_WITH_PROMISE = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export type String = string; +export type SomeObj = { a: string }; + +export interface Spec extends TurboModule { + readonly getValueWithPromise: () => Promise; + readonly getValueWithPromiseDefinedSomewhereElse: () => Promise; + readonly getValueWithPromiseObjDefinedSomewhereElse: () => Promise; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; + +const NATIVE_MODULE_WITH_CALLBACK = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + readonly getValueWithCallback: ( + callback: (value: string, arr: Array>) => void, + ) => void; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); +`; + +const NATIVE_MODULE_WITH_UNION = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export type ChooseInt = 1 | 2 | 3; +export type ChooseFloat = 1.44 | 2.88 | 5.76; +export type ChooseObject = {} | {low: string}; +export type ChooseString = 'One' | 'Two' | 'Three'; + +export interface Spec extends TurboModule { + readonly getUnion: (chooseInt: ChooseInt, chooseFloat: ChooseFloat, chooseObject: ChooseObject, chooseString: ChooseString) => ChooseObject; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const ANDROID_ONLY_NATIVE_MODULE = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule {} + +export default TurboModuleRegistry.getEnforcing( + 'SampleTurboModuleAndroid', +); +`; + +const IOS_ONLY_NATIVE_MODULE = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export enum Quality { + SD, + HD, +} + +export enum Resolution { + Low = 720, + High = 1080, +} + +export enum StringOptions { + One = 'one', + Two = 'two', + Three = 'three', +} + +export interface Spec extends TurboModule { + readonly getEnums: (quality: Quality, resolution?: Resolution, stringOptions: StringOptions) => string; +} + +export default TurboModuleRegistry.getEnforcing( + 'SampleTurboModuleIOS', +); +`; + +const CXX_ONLY_NATIVE_MODULE = ` +/** + * 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 + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export enum Quality { + SD, + HD, +} + +export enum Resolution { + Low = 720, + High = 1080, +} + +export enum StringOptions { + One = 'one', + Two = 'two', + Three = 'three', +} + +export type ChooseInt = 1 | 2 | 3; +export type ChooseFloat = 1.44 | 2.88 | 5.76; +export type ChooseObject = {} | {low: string}; +export type ChooseString = 'One' | 'Two' | 'Three'; + +export type BinaryTreeNode = { + left?: BinaryTreeNode, + value: number, + right?: BinaryTreeNode, +}; + +export type GraphNode = { + label: string, + neighbors?: Array, +}; + +export type CustomDeviceEvent = { + type: string, + level: number, + degree?: number, +}; + +export interface Spec extends TurboModule { + readonly getCallback: () => () => void; + readonly getMixed: (arg: unknown) => unknown; + readonly getEnums: (quality: Quality, resolution?: Resolution, stringOptions: StringOptions) => string; + readonly getBinaryTreeNode: (arg: BinaryTreeNode) => BinaryTreeNode; + readonly getGraphNode: (arg: GraphNode) => GraphNode; + readonly getMap: (arg: {[a: string]: number | null;}) => {[b: string]: number | null;}; + readonly getAnotherMap: (arg: {[key: string]: string}) => {[key: string]: string}; + readonly getUnion: (chooseInt: ChooseInt, chooseFloat: ChooseFloat, chooseObject: ChooseObject, chooseString: ChooseString) => ChooseObject; +} + +export default TurboModuleRegistry.getEnforcing( + 'SampleTurboModuleCxx', +); +`; + +module.exports = { + NATIVE_MODULE_WITH_OBJECT_WITH_OBJECT_DEFINED_IN_FILE_AS_PROPERTY, + NATIVE_MODULE_WITH_ARRAY_WITH_UNION_AND_TOUPLE, + NATIVE_MODULE_WITH_ARRAY2_WITH_UNION_AND_TOUPLE, + NATIVE_MODULE_WITH_FLOAT_AND_INT32, + NATIVE_MODULE_WITH_ALIASES, + NATIVE_MODULE_WITH_NESTED_ALIASES, + NATIVE_MODULE_WITH_NESTED_INTERFACES, + NATIVE_MODULE_WITH_INTERSECTION_TYPES, + NATIVE_MODULE_WITH_PROMISE, + NATIVE_MODULE_WITH_COMPLEX_OBJECTS, + NATIVE_MODULE_WITH_COMPLEX_OBJECTS_WITH_NULLABLE_KEY, + NATIVE_MODULE_WITH_SIMPLE_OBJECT, + NATIVE_MODULE_WITH_UNSAFE_OBJECT, + NATIVE_MODULE_WITH_PARTIALS, + NATIVE_MODULE_WITH_PARTIALS_COMPLEX, + NATIVE_MODULE_WITH_ROOT_TAG, + NATIVE_MODULE_WITH_NULLABLE_PARAM, + NATIVE_MODULE_WITH_BASIC_ARRAY, + NATIVE_MODULE_WITH_BASIC_ARRAY2, + NATIVE_MODULE_WITH_COMPLEX_ARRAY, + NATIVE_MODULE_WITH_COMPLEX_ARRAY2, + NATIVE_MODULE_WITH_ARRAY_WITH_ALIAS, + NATIVE_MODULE_WITH_ARRAY2_WITH_ALIAS, + NATIVE_MODULE_WITH_BASIC_PARAM_TYPES, + NATIVE_MODULE_WITH_CALLBACK, + NATIVE_MODULE_WITH_UNION, + EMPTY_NATIVE_MODULE, + ANDROID_ONLY_NATIVE_MODULE, + IOS_ONLY_NATIVE_MODULE, + CXX_ONLY_NATIVE_MODULE, +}; diff --git a/packages/react-native-codegen/lib/parsers/typescript/modules/index.js b/packages/react-native-codegen/lib/parsers/typescript/modules/index.js new file mode 100644 index 00000000000000..6986108968b6d0 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/typescript/modules/index.js @@ -0,0 +1,404 @@ +/** + * 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 _require = require('../../errors'), + UnsupportedEnumDeclarationParserError = + _require.UnsupportedEnumDeclarationParserError, + UnsupportedGenericParserError = _require.UnsupportedGenericParserError, + UnsupportedTypeAnnotationParserError = + _require.UnsupportedTypeAnnotationParserError; +const _require2 = require('../../parsers-commons'), + parseObjectProperty = _require2.parseObjectProperty; +const _require3 = require('../../parsers-primitives'), + emitArrayType = _require3.emitArrayType, + emitCommonTypes = _require3.emitCommonTypes, + emitDictionary = _require3.emitDictionary, + emitFunction = _require3.emitFunction, + emitPromise = _require3.emitPromise, + emitRootTag = _require3.emitRootTag, + emitUnion = _require3.emitUnion, + translateArrayTypeAnnotation = _require3.translateArrayTypeAnnotation, + typeAliasResolution = _require3.typeAliasResolution, + typeEnumResolution = _require3.typeEnumResolution; +const _require4 = require('../components/componentsUtils'), + flattenProperties = _require4.flattenProperties; +const _require5 = require('../parseTopLevelType'), + flattenIntersectionType = _require5.flattenIntersectionType; +function translateObjectTypeAnnotation( + hasteModuleName, + /** + * TODO(T108222691): Use flow-types for @babel/parser + */ + typeScriptTypeAnnotation, + nullable, + objectMembers, + typeResolutionStatus, + baseTypes, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + parser, +) { + // $FlowFixMe[missing-type-arg] + const properties = objectMembers + .map(property => { + return tryParse(() => { + return parseObjectProperty( + typeScriptTypeAnnotation, + property, + hasteModuleName, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + nullable, + translateTypeAnnotation, + parser, + ); + }); + }) + .filter(Boolean); + let objectTypeAnnotation; + if (baseTypes.length === 0) { + objectTypeAnnotation = { + type: 'ObjectTypeAnnotation', + properties, + }; + } else { + objectTypeAnnotation = { + type: 'ObjectTypeAnnotation', + properties, + baseTypes, + }; + } + return typeAliasResolution( + typeResolutionStatus, + objectTypeAnnotation, + aliasMap, + nullable, + ); +} +function translateTypeReferenceAnnotation( + typeName, + nullable, + typeAnnotation, + hasteModuleName, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + parser, +) { + switch (typeName) { + case 'RootTag': { + return emitRootTag(nullable); + } + case 'Promise': { + return emitPromise( + hasteModuleName, + typeAnnotation, + parser, + nullable, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + translateTypeAnnotation, + ); + } + case 'Array': + case 'ReadonlyArray': { + return emitArrayType( + hasteModuleName, + typeAnnotation, + parser, + types, + aliasMap, + enumMap, + cxxOnly, + nullable, + translateTypeAnnotation, + ); + } + default: { + const commonType = emitCommonTypes( + hasteModuleName, + types, + typeAnnotation, + aliasMap, + enumMap, + tryParse, + cxxOnly, + nullable, + parser, + ); + if (!commonType) { + throw new UnsupportedGenericParserError( + hasteModuleName, + typeAnnotation, + parser, + ); + } + return commonType; + } + } +} +function translateTypeAnnotation( + hasteModuleName, + /** + * TODO(T108222691): Use flow-types for @babel/parser + */ + typeScriptTypeAnnotation, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + parser, +) { + const _parser$getResolvedTy = parser.getResolvedTypeAnnotation( + typeScriptTypeAnnotation, + types, + parser, + ), + nullable = _parser$getResolvedTy.nullable, + typeAnnotation = _parser$getResolvedTy.typeAnnotation, + typeResolutionStatus = _parser$getResolvedTy.typeResolutionStatus; + const resolveTypeaAnnotationFn = parser.getResolveTypeAnnotationFN(); + resolveTypeaAnnotationFn(typeScriptTypeAnnotation, types, parser); + switch (typeAnnotation.type) { + case 'TSArrayType': { + return translateArrayTypeAnnotation( + hasteModuleName, + types, + aliasMap, + enumMap, + cxxOnly, + 'Array', + typeAnnotation.elementType, + nullable, + translateTypeAnnotation, + parser, + ); + } + case 'TSTypeOperator': { + if ( + typeAnnotation.operator === 'readonly' && + typeAnnotation.typeAnnotation.type === 'TSArrayType' + ) { + return translateArrayTypeAnnotation( + hasteModuleName, + types, + aliasMap, + enumMap, + cxxOnly, + 'ReadonlyArray', + typeAnnotation.typeAnnotation.elementType, + nullable, + translateTypeAnnotation, + parser, + ); + } else { + throw new UnsupportedGenericParserError( + hasteModuleName, + typeAnnotation, + parser, + ); + } + } + case 'TSTypeReference': { + return translateTypeReferenceAnnotation( + parser.getTypeAnnotationName(typeAnnotation), + nullable, + typeAnnotation, + hasteModuleName, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + parser, + ); + } + case 'TSInterfaceDeclaration': { + var _typeAnnotation$exten; + const baseTypes = ( + (_typeAnnotation$exten = typeAnnotation.extends) !== null && + _typeAnnotation$exten !== void 0 + ? _typeAnnotation$exten + : [] + ).map(extend => extend.expression.name); + for (const baseType of baseTypes) { + // ensure base types exist and appear in aliasMap + translateTypeAnnotation( + hasteModuleName, + { + type: 'TSTypeReference', + typeName: { + type: 'Identifier', + name: baseType, + }, + }, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + parser, + ); + } + return translateObjectTypeAnnotation( + hasteModuleName, + typeScriptTypeAnnotation, + nullable, + flattenProperties([typeAnnotation], types, parser), + typeResolutionStatus, + baseTypes, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + parser, + ); + } + case 'TSIntersectionType': { + return translateObjectTypeAnnotation( + hasteModuleName, + typeScriptTypeAnnotation, + nullable, + flattenProperties( + flattenIntersectionType(typeAnnotation, types), + types, + parser, + ), + typeResolutionStatus, + [], + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + parser, + ); + } + case 'TSTypeLiteral': { + // if there is TSIndexSignature, then it is a dictionary + if (typeAnnotation.members) { + const indexSignatures = typeAnnotation.members.filter( + member => member.type === 'TSIndexSignature', + ); + if (indexSignatures.length > 0) { + // check the property type to prevent developers from using unsupported types + // the return value from `translateTypeAnnotation` is unused + const propertyType = indexSignatures[0].typeAnnotation; + const valueType = translateTypeAnnotation( + hasteModuleName, + propertyType, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + parser, + ); + // no need to do further checking + return emitDictionary(nullable, valueType); + } + } + return translateObjectTypeAnnotation( + hasteModuleName, + typeScriptTypeAnnotation, + nullable, + typeAnnotation.members, + typeResolutionStatus, + [], + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + parser, + ); + } + case 'TSEnumDeclaration': { + if ( + typeAnnotation.members.some( + m => + m.initializer && + m.initializer.type === 'NumericLiteral' && + !Number.isInteger(m.initializer.value), + ) + ) { + throw new UnsupportedEnumDeclarationParserError( + hasteModuleName, + typeAnnotation, + parser.language(), + ); + } + return typeEnumResolution( + typeAnnotation, + typeResolutionStatus, + nullable, + hasteModuleName, + enumMap, + parser, + ); + } + case 'TSFunctionType': { + return emitFunction( + nullable, + hasteModuleName, + typeAnnotation, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + translateTypeAnnotation, + parser, + ); + } + case 'TSUnionType': { + return emitUnion(nullable, hasteModuleName, typeAnnotation, parser); + } + default: { + const commonType = emitCommonTypes( + hasteModuleName, + types, + typeAnnotation, + aliasMap, + enumMap, + tryParse, + cxxOnly, + nullable, + parser, + ); + if (!commonType) { + throw new UnsupportedTypeAnnotationParserError( + hasteModuleName, + typeAnnotation, + parser.language(), + ); + } + return commonType; + } + } +} +module.exports = { + typeScriptTranslateTypeAnnotation: translateTypeAnnotation, +}; diff --git a/packages/react-native-codegen/lib/parsers/typescript/modules/index.js.flow b/packages/react-native-codegen/lib/parsers/typescript/modules/index.js.flow new file mode 100644 index 00000000000000..ef87bd26ce309c --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/typescript/modules/index.js.flow @@ -0,0 +1,413 @@ +/** + * 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 { + NamedShape, + NativeModuleAliasMap, + NativeModuleBaseTypeAnnotation, + NativeModuleEnumMap, + NativeModuleTypeAnnotation, + Nullable, +} from '../../../CodegenSchema'; +import type {Parser} from '../../parser'; +import type { + ParserErrorCapturer, + TypeDeclarationMap, + TypeResolutionStatus, +} from '../../utils'; + +const { + UnsupportedEnumDeclarationParserError, + UnsupportedGenericParserError, + UnsupportedTypeAnnotationParserError, +} = require('../../errors'); +const {parseObjectProperty} = require('../../parsers-commons'); +const { + emitArrayType, + emitCommonTypes, + emitDictionary, + emitFunction, + emitPromise, + emitRootTag, + emitUnion, + translateArrayTypeAnnotation, + typeAliasResolution, + typeEnumResolution, +} = require('../../parsers-primitives'); +const {flattenProperties} = require('../components/componentsUtils'); +const {flattenIntersectionType} = require('../parseTopLevelType'); + +function translateObjectTypeAnnotation( + hasteModuleName: string, + /** + * TODO(T108222691): Use flow-types for @babel/parser + */ + typeScriptTypeAnnotation: $FlowFixMe, + nullable: boolean, + objectMembers: $ReadOnlyArray<$FlowFixMe>, + typeResolutionStatus: TypeResolutionStatus, + baseTypes: $ReadOnlyArray, + types: TypeDeclarationMap, + aliasMap: {...NativeModuleAliasMap}, + enumMap: {...NativeModuleEnumMap}, + tryParse: ParserErrorCapturer, + cxxOnly: boolean, + parser: Parser, +): Nullable { + // $FlowFixMe[missing-type-arg] + const properties = objectMembers + .map>>(property => { + return tryParse(() => { + return parseObjectProperty( + typeScriptTypeAnnotation, + property, + hasteModuleName, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + nullable, + translateTypeAnnotation, + parser, + ); + }); + }) + .filter(Boolean); + + let objectTypeAnnotation; + if (baseTypes.length === 0) { + objectTypeAnnotation = { + type: 'ObjectTypeAnnotation', + properties, + }; + } else { + objectTypeAnnotation = { + type: 'ObjectTypeAnnotation', + properties, + baseTypes, + }; + } + + return typeAliasResolution( + typeResolutionStatus, + objectTypeAnnotation, + aliasMap, + nullable, + ); +} + +function translateTypeReferenceAnnotation( + typeName: string, + nullable: boolean, + typeAnnotation: $FlowFixMe, + hasteModuleName: string, + types: TypeDeclarationMap, + aliasMap: {...NativeModuleAliasMap}, + enumMap: {...NativeModuleEnumMap}, + tryParse: ParserErrorCapturer, + cxxOnly: boolean, + parser: Parser, +): Nullable { + switch (typeName) { + case 'RootTag': { + return emitRootTag(nullable); + } + case 'Promise': { + return emitPromise( + hasteModuleName, + typeAnnotation, + parser, + nullable, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + translateTypeAnnotation, + ); + } + case 'Array': + case 'ReadonlyArray': { + return emitArrayType( + hasteModuleName, + typeAnnotation, + parser, + types, + aliasMap, + enumMap, + cxxOnly, + nullable, + translateTypeAnnotation, + ); + } + default: { + const commonType = emitCommonTypes( + hasteModuleName, + types, + typeAnnotation, + aliasMap, + enumMap, + tryParse, + cxxOnly, + nullable, + parser, + ); + + if (!commonType) { + throw new UnsupportedGenericParserError( + hasteModuleName, + typeAnnotation, + parser, + ); + } + return commonType; + } + } +} +function translateTypeAnnotation( + hasteModuleName: string, + /** + * TODO(T108222691): Use flow-types for @babel/parser + */ + typeScriptTypeAnnotation: $FlowFixMe, + types: TypeDeclarationMap, + aliasMap: {...NativeModuleAliasMap}, + enumMap: {...NativeModuleEnumMap}, + tryParse: ParserErrorCapturer, + cxxOnly: boolean, + parser: Parser, +): Nullable { + const {nullable, typeAnnotation, typeResolutionStatus} = + parser.getResolvedTypeAnnotation(typeScriptTypeAnnotation, types, parser); + const resolveTypeaAnnotationFn = parser.getResolveTypeAnnotationFN(); + resolveTypeaAnnotationFn(typeScriptTypeAnnotation, types, parser); + + switch (typeAnnotation.type) { + case 'TSArrayType': { + return translateArrayTypeAnnotation( + hasteModuleName, + types, + aliasMap, + enumMap, + cxxOnly, + 'Array', + typeAnnotation.elementType, + nullable, + translateTypeAnnotation, + parser, + ); + } + case 'TSTypeOperator': { + if ( + typeAnnotation.operator === 'readonly' && + typeAnnotation.typeAnnotation.type === 'TSArrayType' + ) { + return translateArrayTypeAnnotation( + hasteModuleName, + types, + aliasMap, + enumMap, + cxxOnly, + 'ReadonlyArray', + typeAnnotation.typeAnnotation.elementType, + nullable, + translateTypeAnnotation, + parser, + ); + } else { + throw new UnsupportedGenericParserError( + hasteModuleName, + typeAnnotation, + parser, + ); + } + } + case 'TSTypeReference': { + return translateTypeReferenceAnnotation( + parser.getTypeAnnotationName(typeAnnotation), + nullable, + typeAnnotation, + hasteModuleName, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + parser, + ); + } + case 'TSInterfaceDeclaration': { + const baseTypes = (typeAnnotation.extends ?? []).map( + extend => extend.expression.name, + ); + for (const baseType of baseTypes) { + // ensure base types exist and appear in aliasMap + translateTypeAnnotation( + hasteModuleName, + { + type: 'TSTypeReference', + typeName: {type: 'Identifier', name: baseType}, + }, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + parser, + ); + } + + return translateObjectTypeAnnotation( + hasteModuleName, + typeScriptTypeAnnotation, + nullable, + flattenProperties([typeAnnotation], types, parser), + typeResolutionStatus, + baseTypes, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + parser, + ); + } + case 'TSIntersectionType': { + return translateObjectTypeAnnotation( + hasteModuleName, + typeScriptTypeAnnotation, + nullable, + flattenProperties( + flattenIntersectionType(typeAnnotation, types), + types, + parser, + ), + typeResolutionStatus, + [], + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + parser, + ); + } + case 'TSTypeLiteral': { + // if there is TSIndexSignature, then it is a dictionary + if (typeAnnotation.members) { + const indexSignatures = typeAnnotation.members.filter( + member => member.type === 'TSIndexSignature', + ); + if (indexSignatures.length > 0) { + // check the property type to prevent developers from using unsupported types + // the return value from `translateTypeAnnotation` is unused + const propertyType = indexSignatures[0].typeAnnotation; + const valueType = translateTypeAnnotation( + hasteModuleName, + propertyType, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + parser, + ); + // no need to do further checking + return emitDictionary(nullable, valueType); + } + } + + return translateObjectTypeAnnotation( + hasteModuleName, + typeScriptTypeAnnotation, + nullable, + typeAnnotation.members, + typeResolutionStatus, + [], + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + parser, + ); + } + case 'TSEnumDeclaration': { + if ( + typeAnnotation.members.some( + m => + m.initializer && + m.initializer.type === 'NumericLiteral' && + !Number.isInteger(m.initializer.value), + ) + ) { + throw new UnsupportedEnumDeclarationParserError( + hasteModuleName, + typeAnnotation, + parser.language(), + ); + } + return typeEnumResolution( + typeAnnotation, + typeResolutionStatus, + nullable, + hasteModuleName, + enumMap, + parser, + ); + } + case 'TSFunctionType': { + return emitFunction( + nullable, + hasteModuleName, + typeAnnotation, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + translateTypeAnnotation, + parser, + ); + } + case 'TSUnionType': { + return emitUnion(nullable, hasteModuleName, typeAnnotation, parser); + } + default: { + const commonType = emitCommonTypes( + hasteModuleName, + types, + typeAnnotation, + aliasMap, + enumMap, + tryParse, + cxxOnly, + nullable, + parser, + ); + + if (!commonType) { + throw new UnsupportedTypeAnnotationParserError( + hasteModuleName, + typeAnnotation, + parser.language(), + ); + } + return commonType; + } + } +} + +module.exports = { + typeScriptTranslateTypeAnnotation: translateTypeAnnotation, +}; diff --git a/packages/react-native-codegen/lib/parsers/typescript/parseTopLevelType.js b/packages/react-native-codegen/lib/parsers/typescript/parseTopLevelType.js new file mode 100644 index 00000000000000..0ef998d8920c00 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/typescript/parseTopLevelType.js @@ -0,0 +1,203 @@ +/** + * 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'; + +function getValueFromTypes(value, types) { + switch (value.type) { + case 'TSTypeReference': + if (types[value.typeName.name]) { + return getValueFromTypes(types[value.typeName.name], types); + } else { + return value; + } + case 'TSTypeAliasDeclaration': + return getValueFromTypes(value.typeAnnotation, types); + default: + return value; + } +} +function isNull(t) { + return t.type === 'TSNullKeyword' || t.type === 'TSUndefinedKeyword'; +} +function isNullOrVoid(t) { + return isNull(t) || t.type === 'TSVoidKeyword'; +} +function couldBeNumericLiteral(type) { + return type === 'Literal' || type === 'NumericLiteral'; +} +function couldBeSimpleLiteral(type) { + return ( + couldBeNumericLiteral(type) || + type === 'StringLiteral' || + type === 'BooleanLiteral' + ); +} +function evaluateLiteral(literalNode) { + const valueType = literalNode.type; + if (valueType === 'TSLiteralType') { + const literal = literalNode.literal; + if (couldBeSimpleLiteral(literal.type)) { + if ( + typeof literal.value === 'string' || + typeof literal.value === 'number' || + typeof literal.value === 'boolean' + ) { + return literal.value; + } + } else if ( + literal.type === 'UnaryExpression' && + literal.operator === '-' && + couldBeNumericLiteral(literal.argument.type) && + typeof literal.argument.value === 'number' + ) { + return -literal.argument.value; + } + } else if (isNull(literalNode)) { + return null; + } + throw new Error( + 'The default value in WithDefault must be string, number, boolean or null .', + ); +} +function handleUnionAndParen(type, result, knownTypes) { + switch (type.type) { + case 'TSParenthesizedType': { + handleUnionAndParen(type.typeAnnotation, result, knownTypes); + break; + } + case 'TSUnionType': { + // the order is important + // result.optional must be set first + for (const t of type.types) { + if (isNullOrVoid(t)) { + result.optional = true; + } + } + for (const t of type.types) { + if (!isNullOrVoid(t)) { + handleUnionAndParen(t, result, knownTypes); + } + } + break; + } + case 'TSTypeReference': + if (type.typeName.name === 'Readonly') { + handleUnionAndParen(type.typeParameters.params[0], result, knownTypes); + } else if (type.typeName.name === 'WithDefault') { + if (result.optional) { + throw new Error( + 'WithDefault<> is optional and does not need to be marked as optional. Please remove the union of undefined and/or null', + ); + } + if (type.typeParameters.params.length !== 2) { + throw new Error( + 'WithDefault requires two parameters: type and default value.', + ); + } + if (result.defaultValue !== undefined) { + throw new Error( + 'Multiple WithDefault is not allowed nested or in a union type.', + ); + } + result.optional = true; + result.defaultValue = evaluateLiteral(type.typeParameters.params[1]); + handleUnionAndParen(type.typeParameters.params[0], result, knownTypes); + } else if (!knownTypes) { + result.unions.push(type); + } else { + const resolvedType = getValueFromTypes(type, knownTypes); + if ( + resolvedType.type === 'TSTypeReference' && + resolvedType.typeName.name === type.typeName.name + ) { + result.unions.push(type); + } else { + handleUnionAndParen(resolvedType, result, knownTypes); + } + } + break; + default: + result.unions.push(type); + } +} +function parseTopLevelType(type, knownTypes) { + let result = { + unions: [], + optional: false, + }; + handleUnionAndParen(type, result, knownTypes); + if (result.unions.length === 0) { + throw new Error('Union type could not be just null or undefined.'); + } else if (result.unions.length === 1) { + return { + type: result.unions[0], + optional: result.optional, + defaultValue: result.defaultValue, + }; + } else { + return { + type: { + type: 'TSUnionType', + types: result.unions, + }, + optional: result.optional, + defaultValue: result.defaultValue, + }; + } +} +function handleIntersectionAndParen(type, result, knownTypes) { + switch (type.type) { + case 'TSParenthesizedType': { + handleIntersectionAndParen(type.typeAnnotation, result, knownTypes); + break; + } + case 'TSIntersectionType': { + for (const t of type.types) { + handleIntersectionAndParen(t, result, knownTypes); + } + break; + } + case 'TSTypeReference': + if (type.typeName.name === 'Readonly') { + handleIntersectionAndParen( + type.typeParameters.params[0], + result, + knownTypes, + ); + } else if (type.typeName.name === 'WithDefault') { + throw new Error('WithDefault<> is now allowed in intersection types.'); + } else if (!knownTypes) { + result.push(type); + } else { + const resolvedType = getValueFromTypes(type, knownTypes); + if ( + resolvedType.type === 'TSTypeReference' && + resolvedType.typeName.name === type.typeName.name + ) { + result.push(type); + } else { + handleIntersectionAndParen(resolvedType, result, knownTypes); + } + } + break; + default: + result.push(type); + } +} +function flattenIntersectionType(type, knownTypes) { + const result = []; + handleIntersectionAndParen(type, result, knownTypes); + return result; +} +module.exports = { + parseTopLevelType, + flattenIntersectionType, +}; diff --git a/packages/react-native-codegen/lib/parsers/typescript/parseTopLevelType.js.flow b/packages/react-native-codegen/lib/parsers/typescript/parseTopLevelType.js.flow new file mode 100644 index 00000000000000..eb43a78a0de9d1 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/typescript/parseTopLevelType.js.flow @@ -0,0 +1,243 @@ +/** + * 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 {TypeDeclarationMap} from '../utils'; + +export type LegalDefaultValues = string | number | boolean | null; + +type TopLevelTypeInternal = { + unions: Array<$FlowFixMe>, + optional: boolean, + defaultValue?: LegalDefaultValues, +}; + +export type TopLevelType = { + type: $FlowFixMe, + optional: boolean, + defaultValue?: LegalDefaultValues, +}; + +function getValueFromTypes( + value: $FlowFixMe, + types: TypeDeclarationMap, +): $FlowFixMe { + switch (value.type) { + case 'TSTypeReference': + if (types[value.typeName.name]) { + return getValueFromTypes(types[value.typeName.name], types); + } else { + return value; + } + case 'TSTypeAliasDeclaration': + return getValueFromTypes(value.typeAnnotation, types); + default: + return value; + } +} + +function isNull(t: $FlowFixMe) { + return t.type === 'TSNullKeyword' || t.type === 'TSUndefinedKeyword'; +} + +function isNullOrVoid(t: $FlowFixMe) { + return isNull(t) || t.type === 'TSVoidKeyword'; +} + +function couldBeNumericLiteral(type: string) { + return type === 'Literal' || type === 'NumericLiteral'; +} + +function couldBeSimpleLiteral(type: string) { + return ( + couldBeNumericLiteral(type) || + type === 'StringLiteral' || + type === 'BooleanLiteral' + ); +} + +function evaluateLiteral( + literalNode: $FlowFixMe, +): string | number | boolean | null { + const valueType = literalNode.type; + if (valueType === 'TSLiteralType') { + const literal = literalNode.literal; + if (couldBeSimpleLiteral(literal.type)) { + if ( + typeof literal.value === 'string' || + typeof literal.value === 'number' || + typeof literal.value === 'boolean' + ) { + return literal.value; + } + } else if ( + literal.type === 'UnaryExpression' && + literal.operator === '-' && + couldBeNumericLiteral(literal.argument.type) && + typeof literal.argument.value === 'number' + ) { + return -literal.argument.value; + } + } else if (isNull(literalNode)) { + return null; + } + + throw new Error( + 'The default value in WithDefault must be string, number, boolean or null .', + ); +} + +function handleUnionAndParen( + type: $FlowFixMe, + result: TopLevelTypeInternal, + knownTypes?: TypeDeclarationMap, +): void { + switch (type.type) { + case 'TSParenthesizedType': { + handleUnionAndParen(type.typeAnnotation, result, knownTypes); + break; + } + case 'TSUnionType': { + // the order is important + // result.optional must be set first + for (const t of type.types) { + if (isNullOrVoid(t)) { + result.optional = true; + } + } + for (const t of type.types) { + if (!isNullOrVoid(t)) { + handleUnionAndParen(t, result, knownTypes); + } + } + break; + } + case 'TSTypeReference': + if (type.typeName.name === 'Readonly') { + handleUnionAndParen(type.typeParameters.params[0], result, knownTypes); + } else if (type.typeName.name === 'WithDefault') { + if (result.optional) { + throw new Error( + 'WithDefault<> is optional and does not need to be marked as optional. Please remove the union of undefined and/or null', + ); + } + if (type.typeParameters.params.length !== 2) { + throw new Error( + 'WithDefault requires two parameters: type and default value.', + ); + } + if (result.defaultValue !== undefined) { + throw new Error( + 'Multiple WithDefault is not allowed nested or in a union type.', + ); + } + result.optional = true; + result.defaultValue = evaluateLiteral(type.typeParameters.params[1]); + handleUnionAndParen(type.typeParameters.params[0], result, knownTypes); + } else if (!knownTypes) { + result.unions.push(type); + } else { + const resolvedType = getValueFromTypes(type, knownTypes); + if ( + resolvedType.type === 'TSTypeReference' && + resolvedType.typeName.name === type.typeName.name + ) { + result.unions.push(type); + } else { + handleUnionAndParen(resolvedType, result, knownTypes); + } + } + break; + default: + result.unions.push(type); + } +} + +function parseTopLevelType( + type: $FlowFixMe, + knownTypes?: TypeDeclarationMap, +): TopLevelType { + let result: TopLevelTypeInternal = {unions: [], optional: false}; + handleUnionAndParen(type, result, knownTypes); + if (result.unions.length === 0) { + throw new Error('Union type could not be just null or undefined.'); + } else if (result.unions.length === 1) { + return { + type: result.unions[0], + optional: result.optional, + defaultValue: result.defaultValue, + }; + } else { + return { + type: {type: 'TSUnionType', types: result.unions}, + optional: result.optional, + defaultValue: result.defaultValue, + }; + } +} + +function handleIntersectionAndParen( + type: $FlowFixMe, + result: Array<$FlowFixMe>, + knownTypes?: TypeDeclarationMap, +): void { + switch (type.type) { + case 'TSParenthesizedType': { + handleIntersectionAndParen(type.typeAnnotation, result, knownTypes); + break; + } + case 'TSIntersectionType': { + for (const t of type.types) { + handleIntersectionAndParen(t, result, knownTypes); + } + break; + } + case 'TSTypeReference': + if (type.typeName.name === 'Readonly') { + handleIntersectionAndParen( + type.typeParameters.params[0], + result, + knownTypes, + ); + } else if (type.typeName.name === 'WithDefault') { + throw new Error('WithDefault<> is now allowed in intersection types.'); + } else if (!knownTypes) { + result.push(type); + } else { + const resolvedType = getValueFromTypes(type, knownTypes); + if ( + resolvedType.type === 'TSTypeReference' && + resolvedType.typeName.name === type.typeName.name + ) { + result.push(type); + } else { + handleIntersectionAndParen(resolvedType, result, knownTypes); + } + } + break; + default: + result.push(type); + } +} + +function flattenIntersectionType( + type: $FlowFixMe, + knownTypes?: TypeDeclarationMap, +): Array<$FlowFixMe> { + const result: Array<$FlowFixMe> = []; + handleIntersectionAndParen(type, result, knownTypes); + return result; +} + +module.exports = { + parseTopLevelType, + flattenIntersectionType, +}; diff --git a/packages/react-native-codegen/lib/parsers/typescript/parser.d.ts b/packages/react-native-codegen/lib/parsers/typescript/parser.d.ts new file mode 100644 index 00000000000000..eeec1a78a6d1fb --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/typescript/parser.d.ts @@ -0,0 +1,17 @@ +/** + * 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. + */ + +import type { Parser } from '../parser'; +import type { SchemaType } from '../../CodegenSchema'; +import type { ParserType } from '../errors'; + +export declare class TypeScriptParser implements Parser { + language(): ParserType; + parseFile(filename: string): SchemaType; + parseString(contents: string, filename?: string): SchemaType; + parseModuleFixture(filename: string): SchemaType; +} diff --git a/packages/react-native-codegen/lib/parsers/typescript/parser.js b/packages/react-native-codegen/lib/parsers/typescript/parser.js new file mode 100644 index 00000000000000..c4340deaaac553 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/typescript/parser.js @@ -0,0 +1,531 @@ +/** + * 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'; + +function _defineProperty(e, r, t) { + return ( + (r = _toPropertyKey(r)) in e + ? Object.defineProperty(e, r, { + value: t, + enumerable: !0, + configurable: !0, + writable: !0, + }) + : (e[r] = t), + e + ); +} +function _toPropertyKey(t) { + var i = _toPrimitive(t, 'string'); + return 'symbol' == typeof i ? i : i + ''; +} +function _toPrimitive(t, r) { + if ('object' != typeof t || !t) return t; + var e = t[Symbol.toPrimitive]; + if (void 0 !== e) { + var i = e.call(t, r || 'default'); + if ('object' != typeof i) return i; + throw new TypeError('@@toPrimitive must return a primitive value.'); + } + return ('string' === r ? String : Number)(t); +} +const _require = require('../errors'), + UnsupportedObjectPropertyTypeAnnotationParserError = + _require.UnsupportedObjectPropertyTypeAnnotationParserError; +const _require2 = require('../parsers-commons.js'), + buildModuleSchema = _require2.buildModuleSchema, + buildPropSchema = _require2.buildPropSchema, + buildSchema = _require2.buildSchema, + extendsForProp = _require2.extendsForProp, + handleGenericTypeAnnotation = _require2.handleGenericTypeAnnotation; +const _require3 = require('../parsers-primitives'), + Visitor = _require3.Visitor; +const _require4 = require('../schema.js'), + wrapComponentSchema = _require4.wrapComponentSchema; +const _require5 = require('./components'), + buildComponentSchema = _require5.buildComponentSchema; +const _require6 = require('./components/componentsUtils'), + flattenProperties = _require6.flattenProperties, + getSchemaInfo = _require6.getSchemaInfo, + getTypeAnnotation = _require6.getTypeAnnotation; +const _require7 = require('./modules'), + typeScriptTranslateTypeAnnotation = + _require7.typeScriptTranslateTypeAnnotation; +const _require8 = require('./parseTopLevelType'), + parseTopLevelType = _require8.parseTopLevelType; +// $FlowFixMe[untyped-import] Use flow-types for @babel/parser +const babelParser = require('@babel/parser'); +const fs = require('fs'); +const invariant = require('invariant'); +class TypeScriptParser { + constructor() { + _defineProperty( + this, + 'typeParameterInstantiation', + 'TSTypeParameterInstantiation', + ); + _defineProperty(this, 'typeAlias', 'TSTypeAliasDeclaration'); + _defineProperty(this, 'enumDeclaration', 'TSEnumDeclaration'); + _defineProperty(this, 'interfaceDeclaration', 'TSInterfaceDeclaration'); + _defineProperty(this, 'nullLiteralTypeAnnotation', 'TSNullKeyword'); + _defineProperty( + this, + 'undefinedLiteralTypeAnnotation', + 'TSUndefinedKeyword', + ); + } + isProperty(property) { + return property.type === 'TSPropertySignature'; + } + getKeyName(property, hasteModuleName) { + if (!this.isProperty(property)) { + throw new UnsupportedObjectPropertyTypeAnnotationParserError( + hasteModuleName, + property, + property.type, + this.language(), + ); + } + return property.key.name; + } + language() { + return 'TypeScript'; + } + getTypeAnnotationName(typeAnnotation) { + var _typeAnnotation$typeN; + return typeAnnotation === null || + typeAnnotation === void 0 || + (_typeAnnotation$typeN = typeAnnotation.typeName) === null || + _typeAnnotation$typeN === void 0 + ? void 0 + : _typeAnnotation$typeN.name; + } + checkIfInvalidModule(typeArguments) { + return ( + typeArguments.type !== 'TSTypeParameterInstantiation' || + typeArguments.params.length !== 1 || + typeArguments.params[0].type !== 'TSTypeReference' || + typeArguments.params[0].typeName.name !== 'Spec' + ); + } + remapUnionTypeAnnotationMemberNames(membersTypes) { + const remapLiteral = item => { + return item.literal + ? item.literal.type + .replace('NumericLiteral', 'NumberTypeAnnotation') + .replace('StringLiteral', 'StringTypeAnnotation') + : 'ObjectTypeAnnotation'; + }; + return [...new Set(membersTypes.map(remapLiteral))]; + } + parseFile(filename) { + const contents = fs.readFileSync(filename, 'utf8'); + return this.parseString(contents, filename); + } + parseString(contents, filename) { + return buildSchema( + contents, + filename, + wrapComponentSchema, + buildComponentSchema, + buildModuleSchema, + Visitor, + this, + typeScriptTranslateTypeAnnotation, + ); + } + parseModuleFixture(filename) { + const contents = fs.readFileSync(filename, 'utf8'); + return this.parseString(contents, 'path/NativeSampleTurboModule.ts'); + } + getAst(contents, filename) { + return babelParser.parse(contents, { + sourceType: 'module', + plugins: ['typescript'], + }).program; + } + getFunctionTypeAnnotationParameters(functionTypeAnnotation) { + return functionTypeAnnotation.parameters; + } + getFunctionNameFromParameter(parameter) { + return parameter.typeAnnotation; + } + getParameterName(parameter) { + return parameter.name; + } + getParameterTypeAnnotation(parameter) { + return parameter.typeAnnotation.typeAnnotation; + } + getFunctionTypeAnnotationReturnType(functionTypeAnnotation) { + return functionTypeAnnotation.typeAnnotation.typeAnnotation; + } + parseEnumMembersType(typeAnnotation) { + var _typeAnnotation$membe; + const enumInitializer = + (_typeAnnotation$membe = typeAnnotation.members[0]) === null || + _typeAnnotation$membe === void 0 + ? void 0 + : _typeAnnotation$membe.initializer; + const enumMembersType = + !enumInitializer || enumInitializer.type === 'StringLiteral' + ? 'StringTypeAnnotation' + : enumInitializer.type === 'NumericLiteral' + ? 'NumberTypeAnnotation' + : null; + if (!enumMembersType) { + throw new Error( + 'Enum values must be either blank, number, or string values.', + ); + } + return enumMembersType; + } + validateEnumMembersSupported(typeAnnotation, enumMembersType) { + if (!typeAnnotation.members || typeAnnotation.members.length === 0) { + throw new Error('Enums should have at least one member.'); + } + const enumInitializerType = + enumMembersType === 'StringTypeAnnotation' + ? 'StringLiteral' + : enumMembersType === 'NumberTypeAnnotation' + ? 'NumericLiteral' + : null; + typeAnnotation.members.forEach(member => { + var _member$initializer$t, _member$initializer; + if ( + ((_member$initializer$t = + (_member$initializer = member.initializer) === null || + _member$initializer === void 0 + ? void 0 + : _member$initializer.type) !== null && + _member$initializer$t !== void 0 + ? _member$initializer$t + : 'StringLiteral') !== enumInitializerType + ) { + throw new Error( + 'Enum values can not be mixed. They all must be either blank, number, or string values.', + ); + } + }); + } + parseEnumMembers(typeAnnotation) { + return typeAnnotation.members.map(member => { + var _member$initializer$v, _member$initializer2; + return { + name: member.id.name, + value: + (_member$initializer$v = + (_member$initializer2 = member.initializer) === null || + _member$initializer2 === void 0 + ? void 0 + : _member$initializer2.value) !== null && + _member$initializer$v !== void 0 + ? _member$initializer$v + : member.id.name, + }; + }); + } + isModuleInterface(node) { + var _node$extends; + return ( + node.type === 'TSInterfaceDeclaration' && + ((_node$extends = node.extends) === null || _node$extends === void 0 + ? void 0 + : _node$extends.length) === 1 && + node.extends[0].type === 'TSExpressionWithTypeArguments' && + node.extends[0].expression.name === 'TurboModule' + ); + } + isGenericTypeAnnotation(type) { + return type === 'TSTypeReference'; + } + extractAnnotatedElement(typeAnnotation, types) { + return types[typeAnnotation.typeParameters.params[0].typeName.name]; + } + + /** + * TODO(T108222691): Use flow-types for @babel/parser + */ + getTypes(ast) { + return ast.body.reduce((types, node) => { + switch (node.type) { + case 'ExportNamedDeclaration': { + if (node.declaration) { + switch (node.declaration.type) { + case 'TSTypeAliasDeclaration': + case 'TSInterfaceDeclaration': + case 'TSEnumDeclaration': { + types[node.declaration.id.name] = node.declaration; + break; + } + } + } + break; + } + case 'TSTypeAliasDeclaration': + case 'TSInterfaceDeclaration': + case 'TSEnumDeclaration': { + types[node.id.name] = node; + break; + } + } + return types; + }, {}); + } + callExpressionTypeParameters(callExpression) { + return callExpression.typeParameters || null; + } + computePartialProperties( + properties, + hasteModuleName, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + ) { + return properties.map(prop => { + return { + name: prop.key.name, + optional: true, + typeAnnotation: typeScriptTranslateTypeAnnotation( + hasteModuleName, + prop.typeAnnotation.typeAnnotation, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + this, + ), + }; + }); + } + functionTypeAnnotation(propertyValueType) { + return ( + propertyValueType === 'TSFunctionType' || + propertyValueType === 'TSMethodSignature' + ); + } + getTypeArgumentParamsFromDeclaration(declaration) { + return declaration.typeParameters.params; + } + + // This FlowFixMe is supposed to refer to typeArgumentParams and funcArgumentParams of generated AST. + getNativeComponentType(typeArgumentParams, funcArgumentParams) { + return { + propsTypeName: typeArgumentParams[0].typeName.name, + componentName: funcArgumentParams[0].value, + }; + } + getAnnotatedElementProperties(annotatedElement) { + return annotatedElement.typeAnnotation.members; + } + bodyProperties(typeAlias) { + return typeAlias.body.body; + } + convertKeywordToTypeAnnotation(keyword) { + switch (keyword) { + case 'TSBooleanKeyword': + return 'BooleanTypeAnnotation'; + case 'TSNumberKeyword': + return 'NumberTypeAnnotation'; + case 'TSVoidKeyword': + return 'VoidTypeAnnotation'; + case 'TSStringKeyword': + return 'StringTypeAnnotation'; + case 'TSUnknownKeyword': + return 'MixedTypeAnnotation'; + } + return keyword; + } + argumentForProp(prop) { + return prop.expression; + } + nameForArgument(prop) { + return prop.expression.name; + } + isOptionalProperty(property) { + return property.optional || false; + } + getGetSchemaInfoFN() { + return getSchemaInfo; + } + getTypeAnnotationFromProperty(property) { + return property.typeAnnotation.typeAnnotation; + } + getGetTypeAnnotationFN() { + return getTypeAnnotation; + } + getResolvedTypeAnnotation( + // TODO(T108222691): Use flow-types for @babel/parser + typeAnnotation, + types, + parser, + ) { + invariant( + typeAnnotation != null, + 'resolveTypeAnnotation(): typeAnnotation cannot be null', + ); + let node = + typeAnnotation.type === 'TSTypeAnnotation' + ? typeAnnotation.typeAnnotation + : typeAnnotation; + let nullable = false; + let typeResolutionStatus = { + successful: false, + }; + for (;;) { + const topLevelType = parseTopLevelType(node); + nullable = nullable || topLevelType.optional; + node = topLevelType.type; + if (node.type !== 'TSTypeReference') { + break; + } + const typeAnnotationName = this.getTypeAnnotationName(node); + const resolvedTypeAnnotation = types[typeAnnotationName]; + if (resolvedTypeAnnotation == null) { + break; + } + const _handleGenericTypeAnn = handleGenericTypeAnnotation( + node, + resolvedTypeAnnotation, + this, + ), + typeAnnotationNode = _handleGenericTypeAnn.typeAnnotation, + status = _handleGenericTypeAnn.typeResolutionStatus; + typeResolutionStatus = status; + node = typeAnnotationNode; + } + return { + nullable: nullable, + typeAnnotation: node, + typeResolutionStatus, + }; + } + getResolveTypeAnnotationFN() { + return (typeAnnotation, types, parser) => { + return this.getResolvedTypeAnnotation(typeAnnotation, types, parser); + }; + } + isEvent(typeAnnotation) { + if (typeAnnotation.type !== 'TSTypeReference') { + return false; + } + const eventNames = new Set(['BubblingEventHandler', 'DirectEventHandler']); + return eventNames.has(this.getTypeAnnotationName(typeAnnotation)); + } + isProp(name, typeAnnotation) { + if (typeAnnotation.type !== 'TSTypeReference') { + return true; + } + const isStyle = + name === 'style' && + typeAnnotation.type === 'GenericTypeAnnotation' && + this.getTypeAnnotationName(typeAnnotation) === 'ViewStyleProp'; + return !isStyle; + } + getProps(typeDefinition, types) { + const extendsProps = []; + const componentPropAsts = []; + const remaining = []; + for (const prop of typeDefinition) { + // find extends + if (prop.type === 'TSExpressionWithTypeArguments') { + const extend = extendsForProp(prop, types, this); + if (extend) { + extendsProps.push(extend); + continue; + } + } + remaining.push(prop); + } + + // find events and props + for (const prop of flattenProperties(remaining, types, this)) { + const topLevelType = parseTopLevelType( + prop.typeAnnotation.typeAnnotation, + types, + ); + if ( + prop.type === 'TSPropertySignature' && + !this.isEvent(topLevelType.type) && + this.isProp(prop.key.name, prop) + ) { + componentPropAsts.push(prop); + } + } + return { + props: componentPropAsts + .map(property => buildPropSchema(property, types, this)) + .filter(Boolean), + extendsProps, + }; + } + getProperties(typeName, types) { + const alias = types[typeName]; + if (!alias) { + throw new Error( + `Failed to find definition for "${typeName}", please check that you have a valid codegen typescript file`, + ); + } + const aliasKind = + alias.type === 'TSInterfaceDeclaration' ? 'interface' : 'type'; + try { + if (aliasKind === 'interface') { + var _alias$extends; + return [ + ...((_alias$extends = alias.extends) !== null && + _alias$extends !== void 0 + ? _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 ${aliasKind} definition for "${typeName}", please check that you have a valid codegen typescript file`, + ); + } + } + nextNodeForTypeAlias(typeAnnotation) { + return typeAnnotation.typeAnnotation; + } + nextNodeForEnum(typeAnnotation) { + return typeAnnotation; + } + genericTypeAnnotationErrorMessage(typeAnnotation) { + return `A non GenericTypeAnnotation must be a type declaration ('${this.typeAlias}'), an interface ('${this.interfaceDeclaration}'), or enum ('${this.enumDeclaration}'). Instead, got the unsupported ${typeAnnotation.type}.`; + } + extractTypeFromTypeAnnotation(typeAnnotation) { + return typeAnnotation.type === 'TSTypeReference' + ? typeAnnotation.typeName.name + : typeAnnotation.type; + } + getObjectProperties(typeAnnotation) { + return typeAnnotation.members; + } + getLiteralValue(option) { + return option.literal.value; + } + getPaperTopLevelNameDeprecated(typeAnnotation) { + return typeAnnotation.typeParameters.params.length > 1 + ? typeAnnotation.typeParameters.params[1].literal.value + : null; + } +} +module.exports = { + TypeScriptParser, +}; diff --git a/packages/react-native-codegen/lib/parsers/typescript/parser.js.flow b/packages/react-native-codegen/lib/parsers/typescript/parser.js.flow new file mode 100644 index 00000000000000..7262b60301bfed --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/typescript/parser.js.flow @@ -0,0 +1,570 @@ +/** + * 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 { + ExtendsPropsShape, + NamedShape, + NativeModuleAliasMap, + NativeModuleEnumMap, + NativeModuleEnumMembers, + NativeModuleEnumMemberType, + NativeModuleParamTypeAnnotation, + Nullable, + PropTypeAnnotation, + SchemaType, + UnionTypeAnnotationMemberType, +} from '../../CodegenSchema'; +import type {ParserType} from '../errors'; +import type { + GetSchemaInfoFN, + GetTypeAnnotationFN, + Parser, + ResolveTypeAnnotationFN, +} from '../parser'; +import type { + ParserErrorCapturer, + PropAST, + TypeDeclarationMap, + TypeResolutionStatus, +} from '../utils'; + +const { + UnsupportedObjectPropertyTypeAnnotationParserError, +} = require('../errors'); +const { + buildModuleSchema, + buildPropSchema, + buildSchema, + extendsForProp, + handleGenericTypeAnnotation, +} = require('../parsers-commons.js'); +const {Visitor} = require('../parsers-primitives'); +const {wrapComponentSchema} = require('../schema.js'); +const {buildComponentSchema} = require('./components'); +const { + flattenProperties, + getSchemaInfo, + getTypeAnnotation, +} = require('./components/componentsUtils'); +const {typeScriptTranslateTypeAnnotation} = require('./modules'); +const {parseTopLevelType} = require('./parseTopLevelType'); +// $FlowFixMe[untyped-import] Use flow-types for @babel/parser +const babelParser = require('@babel/parser'); +const fs = require('fs'); +const invariant = require('invariant'); + +class TypeScriptParser implements Parser { + typeParameterInstantiation: string = 'TSTypeParameterInstantiation'; + typeAlias: string = 'TSTypeAliasDeclaration'; + enumDeclaration: string = 'TSEnumDeclaration'; + interfaceDeclaration: string = 'TSInterfaceDeclaration'; + nullLiteralTypeAnnotation: string = 'TSNullKeyword'; + undefinedLiteralTypeAnnotation: string = 'TSUndefinedKeyword'; + + isProperty(property: $FlowFixMe): boolean { + return property.type === 'TSPropertySignature'; + } + + getKeyName(property: $FlowFixMe, hasteModuleName: string): string { + if (!this.isProperty(property)) { + throw new UnsupportedObjectPropertyTypeAnnotationParserError( + hasteModuleName, + property, + property.type, + this.language(), + ); + } + return property.key.name; + } + + language(): ParserType { + return 'TypeScript'; + } + + getTypeAnnotationName(typeAnnotation: $FlowFixMe): string { + return typeAnnotation?.typeName?.name; + } + + checkIfInvalidModule(typeArguments: $FlowFixMe): boolean { + return ( + typeArguments.type !== 'TSTypeParameterInstantiation' || + typeArguments.params.length !== 1 || + typeArguments.params[0].type !== 'TSTypeReference' || + typeArguments.params[0].typeName.name !== 'Spec' + ); + } + + remapUnionTypeAnnotationMemberNames( + membersTypes: $FlowFixMe[], + ): UnionTypeAnnotationMemberType[] { + const remapLiteral = (item: $FlowFixMe) => { + return item.literal + ? item.literal.type + .replace('NumericLiteral', 'NumberTypeAnnotation') + .replace('StringLiteral', 'StringTypeAnnotation') + : 'ObjectTypeAnnotation'; + }; + + return [...new Set(membersTypes.map(remapLiteral))]; + } + + parseFile(filename: string): SchemaType { + const contents = fs.readFileSync(filename, 'utf8'); + + return this.parseString(contents, filename); + } + + parseString(contents: string, filename: ?string): SchemaType { + return buildSchema( + contents, + filename, + wrapComponentSchema, + buildComponentSchema, + buildModuleSchema, + Visitor, + this, + typeScriptTranslateTypeAnnotation, + ); + } + + parseModuleFixture(filename: string): SchemaType { + const contents = fs.readFileSync(filename, 'utf8'); + + return this.parseString(contents, 'path/NativeSampleTurboModule.ts'); + } + + getAst(contents: string, filename?: ?string): $FlowFixMe { + return babelParser.parse(contents, { + sourceType: 'module', + plugins: ['typescript'], + }).program; + } + + getFunctionTypeAnnotationParameters( + functionTypeAnnotation: $FlowFixMe, + ): $ReadOnlyArray<$FlowFixMe> { + return functionTypeAnnotation.parameters; + } + + getFunctionNameFromParameter( + parameter: NamedShape>, + ): $FlowFixMe { + return parameter.typeAnnotation; + } + + getParameterName(parameter: $FlowFixMe): string { + return parameter.name; + } + + getParameterTypeAnnotation(parameter: $FlowFixMe): $FlowFixMe { + return parameter.typeAnnotation.typeAnnotation; + } + + getFunctionTypeAnnotationReturnType( + functionTypeAnnotation: $FlowFixMe, + ): $FlowFixMe { + return functionTypeAnnotation.typeAnnotation.typeAnnotation; + } + + parseEnumMembersType(typeAnnotation: $FlowFixMe): NativeModuleEnumMemberType { + const enumInitializer = typeAnnotation.members[0]?.initializer; + const enumMembersType: ?NativeModuleEnumMemberType = + !enumInitializer || enumInitializer.type === 'StringLiteral' + ? 'StringTypeAnnotation' + : enumInitializer.type === 'NumericLiteral' + ? 'NumberTypeAnnotation' + : null; + if (!enumMembersType) { + throw new Error( + 'Enum values must be either blank, number, or string values.', + ); + } + return enumMembersType; + } + + validateEnumMembersSupported( + typeAnnotation: $FlowFixMe, + enumMembersType: NativeModuleEnumMemberType, + ): void { + if (!typeAnnotation.members || typeAnnotation.members.length === 0) { + throw new Error('Enums should have at least one member.'); + } + + const enumInitializerType = + enumMembersType === 'StringTypeAnnotation' + ? 'StringLiteral' + : enumMembersType === 'NumberTypeAnnotation' + ? 'NumericLiteral' + : null; + + typeAnnotation.members.forEach(member => { + if ( + (member.initializer?.type ?? 'StringLiteral') !== enumInitializerType + ) { + throw new Error( + 'Enum values can not be mixed. They all must be either blank, number, or string values.', + ); + } + }); + } + + parseEnumMembers(typeAnnotation: $FlowFixMe): NativeModuleEnumMembers { + return typeAnnotation.members.map(member => ({ + name: member.id.name, + value: member.initializer?.value ?? member.id.name, + })); + } + + isModuleInterface(node: $FlowFixMe): boolean { + return ( + node.type === 'TSInterfaceDeclaration' && + node.extends?.length === 1 && + node.extends[0].type === 'TSExpressionWithTypeArguments' && + node.extends[0].expression.name === 'TurboModule' + ); + } + + isGenericTypeAnnotation(type: $FlowFixMe): boolean { + return type === 'TSTypeReference'; + } + + extractAnnotatedElement( + typeAnnotation: $FlowFixMe, + types: TypeDeclarationMap, + ): $FlowFixMe { + return types[typeAnnotation.typeParameters.params[0].typeName.name]; + } + + /** + * TODO(T108222691): Use flow-types for @babel/parser + */ + getTypes(ast: $FlowFixMe): TypeDeclarationMap { + return ast.body.reduce((types, node) => { + switch (node.type) { + case 'ExportNamedDeclaration': { + if (node.declaration) { + switch (node.declaration.type) { + case 'TSTypeAliasDeclaration': + case 'TSInterfaceDeclaration': + case 'TSEnumDeclaration': { + types[node.declaration.id.name] = node.declaration; + break; + } + } + } + break; + } + case 'TSTypeAliasDeclaration': + case 'TSInterfaceDeclaration': + case 'TSEnumDeclaration': { + types[node.id.name] = node; + break; + } + } + return types; + }, {}); + } + + callExpressionTypeParameters(callExpression: $FlowFixMe): $FlowFixMe | null { + return callExpression.typeParameters || null; + } + + computePartialProperties( + properties: Array<$FlowFixMe>, + hasteModuleName: string, + types: TypeDeclarationMap, + aliasMap: {...NativeModuleAliasMap}, + enumMap: {...NativeModuleEnumMap}, + tryParse: ParserErrorCapturer, + cxxOnly: boolean, + ): Array<$FlowFixMe> { + return properties.map(prop => { + return { + name: prop.key.name, + optional: true, + typeAnnotation: typeScriptTranslateTypeAnnotation( + hasteModuleName, + prop.typeAnnotation.typeAnnotation, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + this, + ), + }; + }); + } + + functionTypeAnnotation(propertyValueType: string): boolean { + return ( + propertyValueType === 'TSFunctionType' || + propertyValueType === 'TSMethodSignature' + ); + } + + getTypeArgumentParamsFromDeclaration(declaration: $FlowFixMe): $FlowFixMe { + return declaration.typeParameters.params; + } + + // This FlowFixMe is supposed to refer to typeArgumentParams and funcArgumentParams of generated AST. + getNativeComponentType( + typeArgumentParams: $FlowFixMe, + funcArgumentParams: $FlowFixMe, + ): {[string]: string} { + return { + propsTypeName: typeArgumentParams[0].typeName.name, + componentName: funcArgumentParams[0].value, + }; + } + + getAnnotatedElementProperties(annotatedElement: $FlowFixMe): $FlowFixMe { + return annotatedElement.typeAnnotation.members; + } + + bodyProperties(typeAlias: TypeDeclarationMap): $ReadOnlyArray<$FlowFixMe> { + return typeAlias.body.body; + } + + convertKeywordToTypeAnnotation(keyword: string): string { + switch (keyword) { + case 'TSBooleanKeyword': + return 'BooleanTypeAnnotation'; + case 'TSNumberKeyword': + return 'NumberTypeAnnotation'; + case 'TSVoidKeyword': + return 'VoidTypeAnnotation'; + case 'TSStringKeyword': + return 'StringTypeAnnotation'; + case 'TSUnknownKeyword': + return 'MixedTypeAnnotation'; + } + + return keyword; + } + + argumentForProp(prop: PropAST): $FlowFixMe { + return prop.expression; + } + + nameForArgument(prop: PropAST): $FlowFixMe { + return prop.expression.name; + } + + isOptionalProperty(property: $FlowFixMe): boolean { + return property.optional || false; + } + + getGetSchemaInfoFN(): GetSchemaInfoFN { + return getSchemaInfo; + } + + getTypeAnnotationFromProperty(property: PropAST): $FlowFixMe { + return property.typeAnnotation.typeAnnotation; + } + + getGetTypeAnnotationFN(): GetTypeAnnotationFN { + return getTypeAnnotation; + } + + getResolvedTypeAnnotation( + // TODO(T108222691): Use flow-types for @babel/parser + typeAnnotation: $FlowFixMe, + types: TypeDeclarationMap, + parser: Parser, + ): { + nullable: boolean, + typeAnnotation: $FlowFixMe, + typeResolutionStatus: TypeResolutionStatus, + } { + invariant( + typeAnnotation != null, + 'resolveTypeAnnotation(): typeAnnotation cannot be null', + ); + + let node = + typeAnnotation.type === 'TSTypeAnnotation' + ? typeAnnotation.typeAnnotation + : typeAnnotation; + let nullable = false; + let typeResolutionStatus: TypeResolutionStatus = { + successful: false, + }; + + for (;;) { + const topLevelType = parseTopLevelType(node); + nullable = nullable || topLevelType.optional; + node = topLevelType.type; + + if (node.type !== 'TSTypeReference') { + break; + } + + const typeAnnotationName = this.getTypeAnnotationName(node); + const resolvedTypeAnnotation = types[typeAnnotationName]; + if (resolvedTypeAnnotation == null) { + break; + } + + const {typeAnnotation: typeAnnotationNode, typeResolutionStatus: status} = + handleGenericTypeAnnotation(node, resolvedTypeAnnotation, this); + typeResolutionStatus = status; + node = typeAnnotationNode; + } + + return { + nullable: nullable, + typeAnnotation: node, + typeResolutionStatus, + }; + } + + getResolveTypeAnnotationFN(): ResolveTypeAnnotationFN { + return ( + typeAnnotation: $FlowFixMe, + types: TypeDeclarationMap, + parser: Parser, + ) => { + return this.getResolvedTypeAnnotation(typeAnnotation, types, parser); + }; + } + + isEvent(typeAnnotation: $FlowFixMe): boolean { + if (typeAnnotation.type !== 'TSTypeReference') { + return false; + } + const eventNames = new Set(['BubblingEventHandler', 'DirectEventHandler']); + return eventNames.has(this.getTypeAnnotationName(typeAnnotation)); + } + + isProp(name: string, typeAnnotation: $FlowFixMe): boolean { + if (typeAnnotation.type !== 'TSTypeReference') { + return true; + } + const isStyle = + name === 'style' && + typeAnnotation.type === 'GenericTypeAnnotation' && + this.getTypeAnnotationName(typeAnnotation) === 'ViewStyleProp'; + return !isStyle; + } + + getProps( + typeDefinition: $ReadOnlyArray, + types: TypeDeclarationMap, + ): { + props: $ReadOnlyArray>, + extendsProps: $ReadOnlyArray, + } { + const extendsProps: Array = []; + const componentPropAsts: Array = []; + const remaining: Array = []; + + for (const prop of typeDefinition) { + // find extends + if (prop.type === 'TSExpressionWithTypeArguments') { + const extend = extendsForProp(prop, types, this); + if (extend) { + extendsProps.push(extend); + continue; + } + } + + remaining.push(prop); + } + + // find events and props + for (const prop of flattenProperties(remaining, types, this)) { + const topLevelType = parseTopLevelType( + prop.typeAnnotation.typeAnnotation, + types, + ); + + if ( + prop.type === 'TSPropertySignature' && + !this.isEvent(topLevelType.type) && + this.isProp(prop.key.name, prop) + ) { + componentPropAsts.push(prop); + } + } + + return { + props: componentPropAsts + .map(property => buildPropSchema(property, types, this)) + .filter(Boolean), + extendsProps, + }; + } + + getProperties(typeName: string, types: TypeDeclarationMap): $FlowFixMe { + const alias = types[typeName]; + if (!alias) { + throw new Error( + `Failed to find definition for "${typeName}", please check that you have a valid codegen typescript file`, + ); + } + const aliasKind = + alias.type === 'TSInterfaceDeclaration' ? 'interface' : 'type'; + + try { + 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 ${aliasKind} definition for "${typeName}", please check that you have a valid codegen typescript file`, + ); + } + } + + nextNodeForTypeAlias(typeAnnotation: $FlowFixMe): $FlowFixMe { + return typeAnnotation.typeAnnotation; + } + + nextNodeForEnum(typeAnnotation: $FlowFixMe): $FlowFixMe { + return typeAnnotation; + } + + genericTypeAnnotationErrorMessage(typeAnnotation: $FlowFixMe): string { + return `A non GenericTypeAnnotation must be a type declaration ('${this.typeAlias}'), an interface ('${this.interfaceDeclaration}'), or enum ('${this.enumDeclaration}'). Instead, got the unsupported ${typeAnnotation.type}.`; + } + + extractTypeFromTypeAnnotation(typeAnnotation: $FlowFixMe): string { + return typeAnnotation.type === 'TSTypeReference' + ? typeAnnotation.typeName.name + : typeAnnotation.type; + } + + getObjectProperties(typeAnnotation: $FlowFixMe): $FlowFixMe { + return typeAnnotation.members; + } + + getLiteralValue(option: $FlowFixMe): $FlowFixMe { + return option.literal.value; + } + + getPaperTopLevelNameDeprecated(typeAnnotation: $FlowFixMe): $FlowFixMe { + return typeAnnotation.typeParameters.params.length > 1 + ? typeAnnotation.typeParameters.params[1].literal.value + : null; + } +} + +module.exports = { + TypeScriptParser, +}; diff --git a/packages/react-native-codegen/lib/parsers/utils.js b/packages/react-native-codegen/lib/parsers/utils.js new file mode 100644 index 00000000000000..cad49a1eb7421e --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/utils.js @@ -0,0 +1,171 @@ +/** + * 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 _require = require('./errors'), + ParserError = _require.ParserError; +const path = require('path'); +function extractNativeModuleName(filename) { + // this should drop everything after the file name. For Example it will drop: + // .android.js, .android.ts, .android.tsx, .ios.js, .ios.ts, .ios.tsx, .js, .ts, .tsx + return path.basename(filename).split('.')[0]; +} + +// $FlowFixMe[unclear-type] there's no flowtype for ASTs + +// $FlowFixMe[unclear-type] there's no flowtype for ASTs + +function createParserErrorCapturer() { + // $FlowFixMe[missing-empty-array-annot] + const errors = []; + function guard(fn) { + try { + return fn(); + } catch (error) { + if (!(error instanceof ParserError)) { + throw error; + } + // $FlowFixMe[incompatible-call] + errors.push(error); + return null; + } + } + + // $FlowFixMe[incompatible-return] + return [errors, guard]; +} +function verifyPlatforms(hasteModuleName, moduleName) { + let cxxOnly = false; + const excludedPlatforms = new Set(); + const namesToValidate = [moduleName, hasteModuleName]; + namesToValidate.forEach(name => { + if (name.endsWith('Android')) { + excludedPlatforms.add('iOS'); + return; + } + if (name.endsWith('IOS')) { + excludedPlatforms.add('android'); + return; + } + if (name.endsWith('Windows')) { + excludedPlatforms.add('iOS'); + excludedPlatforms.add('android'); + return; + } + if (name.endsWith('Cxx')) { + cxxOnly = true; + excludedPlatforms.add('iOS'); + excludedPlatforms.add('android'); + return; + } + }); + return { + cxxOnly, + excludedPlatforms: Array.from(excludedPlatforms), + }; +} + +// TODO(T108222691): Use flow-types for @babel/parser +function visit(astNode, visitor) { + const queue = [astNode]; + while (queue.length !== 0) { + let item = queue.shift(); + if (!(typeof item === 'object' && item != null)) { + continue; + } + if ( + typeof item.type === 'string' && + typeof visitor[item.type] === 'function' + ) { + // Don't visit any children + visitor[item.type](item); + } else if (Array.isArray(item)) { + queue.push(...item); + } else { + queue.push(...Object.values(item)); + } + } +} +function getConfigType( + // TODO(T71778680): Flow-type this node. + ast, + Visitor, +) { + let infoMap = { + isComponent: false, + isModule: false, + }; + visit(ast, Visitor(infoMap)); + const isModule = infoMap.isModule, + isComponent = infoMap.isComponent; + if (isModule && isComponent) { + throw new Error( + 'Found type extending "TurboModule" and exported "codegenNativeComponent" declaration in one file. Split them into separated files.', + ); + } + if (isModule) { + return 'module'; + } else if (isComponent) { + return 'component'; + } else { + return 'none'; + } +} + +// TODO(T71778680): Flow-type ASTNodes. +function isModuleRegistryCall(node) { + if (node.type !== 'CallExpression') { + return false; + } + const callExpression = node; + if (callExpression.callee.type !== 'MemberExpression') { + return false; + } + const memberExpression = callExpression.callee; + if ( + !( + memberExpression.object.type === 'Identifier' && + memberExpression.object.name === 'TurboModuleRegistry' + ) + ) { + return false; + } + if ( + !( + memberExpression.property.type === 'Identifier' && + (memberExpression.property.name === 'get' || + memberExpression.property.name === 'getEnforcing') + ) + ) { + return false; + } + if (memberExpression.computed) { + return false; + } + return true; +} +function getSortedObject(unsortedObject) { + return Object.keys(unsortedObject) + .sort() + .reduce((sortedObject, key) => { + sortedObject[key] = unsortedObject[key]; + return sortedObject; + }, {}); +} +module.exports = { + getConfigType, + extractNativeModuleName, + createParserErrorCapturer, + verifyPlatforms, + visit, + isModuleRegistryCall, + getSortedObject, +}; diff --git a/packages/react-native-codegen/lib/parsers/utils.js.flow b/packages/react-native-codegen/lib/parsers/utils.js.flow new file mode 100644 index 00000000000000..2c6ab4996f6135 --- /dev/null +++ b/packages/react-native-codegen/lib/parsers/utils.js.flow @@ -0,0 +1,225 @@ +/** + * 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'; + +const {ParserError} = require('./errors'); +const path = require('path'); + +export type TypeDeclarationMap = {[declarationName: string]: $FlowFixMe}; + +export type TypeResolutionStatus = + | $ReadOnly<{ + type: 'alias' | 'enum', + successful: true, + name: string, + }> + | $ReadOnly<{ + successful: false, + }>; + +function extractNativeModuleName(filename: string): string { + // this should drop everything after the file name. For Example it will drop: + // .android.js, .android.ts, .android.tsx, .ios.js, .ios.ts, .ios.tsx, .js, .ts, .tsx + return path.basename(filename).split('.')[0]; +} + +export type ParserErrorCapturer = (fn: () => T) => ?T; + +// $FlowFixMe[unclear-type] there's no flowtype for ASTs +export type PropAST = Object; + +// $FlowFixMe[unclear-type] there's no flowtype for ASTs +export type ASTNode = Object; + +function createParserErrorCapturer(): [ + Array, + ParserErrorCapturer, +] { + // $FlowFixMe[missing-empty-array-annot] + const errors = []; + function guard(fn: () => T): ?T { + try { + return fn(); + } catch (error) { + if (!(error instanceof ParserError)) { + throw error; + } + // $FlowFixMe[incompatible-call] + errors.push(error); + + return null; + } + } + + // $FlowFixMe[incompatible-return] + return [errors, guard]; +} + +function verifyPlatforms( + hasteModuleName: string, + moduleName: string, +): $ReadOnly<{ + cxxOnly: boolean, + excludedPlatforms: Array<'iOS' | 'android'>, +}> { + let cxxOnly = false; + const excludedPlatforms = new Set<'iOS' | 'android'>(); + const namesToValidate = [moduleName, hasteModuleName]; + + namesToValidate.forEach(name => { + if (name.endsWith('Android')) { + excludedPlatforms.add('iOS'); + return; + } + + if (name.endsWith('IOS')) { + excludedPlatforms.add('android'); + return; + } + + if (name.endsWith('Windows')) { + excludedPlatforms.add('iOS'); + excludedPlatforms.add('android'); + return; + } + + if (name.endsWith('Cxx')) { + cxxOnly = true; + excludedPlatforms.add('iOS'); + excludedPlatforms.add('android'); + return; + } + }); + + return { + cxxOnly, + excludedPlatforms: Array.from(excludedPlatforms), + }; +} + +// TODO(T108222691): Use flow-types for @babel/parser +function visit( + astNode: $FlowFixMe, + visitor: { + [type: string]: (node: $FlowFixMe) => void, + }, +) { + const queue = [astNode]; + while (queue.length !== 0) { + let item = queue.shift(); + + if (!(typeof item === 'object' && item != null)) { + continue; + } + + if ( + typeof item.type === 'string' && + typeof visitor[item.type] === 'function' + ) { + // Don't visit any children + visitor[item.type](item); + } else if (Array.isArray(item)) { + queue.push(...item); + } else { + queue.push(...Object.values(item)); + } + } +} + +function getConfigType( + // TODO(T71778680): Flow-type this node. + ast: $FlowFixMe, + Visitor: ({isComponent: boolean, isModule: boolean}) => { + [type: string]: (node: $FlowFixMe) => void, + }, +): 'module' | 'component' | 'none' { + let infoMap = { + isComponent: false, + isModule: false, + }; + + visit(ast, Visitor(infoMap)); + + const {isModule, isComponent} = infoMap; + if (isModule && isComponent) { + throw new Error( + 'Found type extending "TurboModule" and exported "codegenNativeComponent" declaration in one file. Split them into separated files.', + ); + } + + if (isModule) { + return 'module'; + } else if (isComponent) { + return 'component'; + } else { + return 'none'; + } +} + +// TODO(T71778680): Flow-type ASTNodes. +function isModuleRegistryCall(node: $FlowFixMe): boolean { + if (node.type !== 'CallExpression') { + return false; + } + + const callExpression = node; + + if (callExpression.callee.type !== 'MemberExpression') { + return false; + } + + const memberExpression = callExpression.callee; + if ( + !( + memberExpression.object.type === 'Identifier' && + memberExpression.object.name === 'TurboModuleRegistry' + ) + ) { + return false; + } + + if ( + !( + memberExpression.property.type === 'Identifier' && + (memberExpression.property.name === 'get' || + memberExpression.property.name === 'getEnforcing') + ) + ) { + return false; + } + + if (memberExpression.computed) { + return false; + } + + return true; +} + +function getSortedObject(unsortedObject: {[key: string]: T}): { + [key: string]: T, +} { + return Object.keys(unsortedObject) + .sort() + .reduce((sortedObject: {[key: string]: T}, key: string) => { + sortedObject[key] = unsortedObject[key]; + return sortedObject; + }, {}); +} + +module.exports = { + getConfigType, + extractNativeModuleName, + createParserErrorCapturer, + verifyPlatforms, + visit, + isModuleRegistryCall, + getSortedObject, +};