Parse custom NativeState in Flow (#34753)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/34753

This Diff introduce a the capability to parse custom NativeStates in Flow. To achieve this I also had to define the CodegenSchema.

The parsing follows the exact same rules as props, as initial heuristic. This should allow enough customization for the developers who needs a custom state.

There is only a case I was not able to make it work that is STATE_ALIASED_LOCALLY, from the fixtures. I don't know how diffuse it is and I think we can live with some workarounds for the time being.

This diff also adds tests for the custom Native State Flow Parser.

## Changelog
[General][Added] - Implement custom Native State parsing in Flow

Reviewed By: cortinico

Differential Revision: D39686251

fbshipit-source-id: 446997a39b33b7e9351d5ba12cecaeff33df4d16
This commit is contained in:
Riccardo Cipolleschi
2022-09-26 07:33:07 -07:00
committed by Facebook GitHub Bot
parent 5e79fa8441
commit 925b15351f
10 changed files with 3488 additions and 534 deletions
+3
View File
@@ -80,6 +80,7 @@ export type ComponentShape = $ReadOnly<{
events: $ReadOnlyArray<EventTypeShape>,
props: $ReadOnlyArray<NamedShape<PropTypeAnnotation>>,
commands: $ReadOnlyArray<NamedShape<CommandTypeAnnotation>>,
state?: $ReadOnlyArray<NamedShape<StateTypeAnnotation>>,
}>;
export type OptionsShape = $ReadOnly<{
@@ -185,6 +186,8 @@ export type ReservedPropTypeAnnotation = $ReadOnly<{
| 'EdgeInsetsPrimitive',
}>;
export type StateTypeAnnotation = PropTypeAnnotation;
export type CommandTypeAnnotation = FunctionTypeAnnotation<
CommandParamTypeAnnotation,
VoidTypeAnnotation,
@@ -14,10 +14,19 @@ const {compareSnaps, compareTsArraySnaps} = require('../compareSnaps.js');
const flowFixtures = require('../../flow/components/__test_fixtures__/fixtures.js');
const flowSnaps = require('../../../../src/parsers/flow/components/__tests__/__snapshots__/component-parser-test.js.snap');
const flowExtraCases = [];
const flowExtraCases = [
//TODO: remove these once we implement TypeScript parser for Custom State
'ALL_STATE_TYPES',
'ARRAY_STATE_TYPES',
'COMMANDS_EVENTS_STATE_TYPES_EXPORTED',
'OBJECT_STATE_TYPES',
];
const tsFixtures = require('../../typescript/components/__test_fixtures__/fixtures.js');
const tsSnaps = require('../../../../src/parsers/typescript/components/__tests__/__snapshots__/typescript-component-parser-test.js.snap');
const tsExtraCases = ['ARRAY2_PROP_TYPES_NO_EVENTS'];
const tsExtraCases = ['ARRAY2_PROP_TYPES_NO_EVENTS'].concat([
//TODO: remove these once we implement TypeScript parser for Custom State
'COMMANDS_AND_EVENTS_TYPES_EXPORTED',
]);
const ignoredCases = ['ARRAY_PROP_TYPES_NO_EVENTS'];
compareSnaps(
@@ -579,6 +579,402 @@ export default (codegenNativeComponent<ModuleProps>(
): HostComponent<ModuleProps>);
`;
// === STATE ===
const NULLABLE_STATE_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,
|}>;
export type ModuleNativeState = $ReadOnly<{|
nullable_with_default: ?WithDefault<Float, 1.0>,
|}>;
export default (codegenNativeComponent<ModuleProps>(
'Module',
): HostComponent<ModuleProps>);
`;
const NON_OPTIONAL_KEY_STATE_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<Float, 1.0>,
|}>;
export type ModuleNativeState = $ReadOnly<{|
required_key_with_default: WithDefault<Float, 1.0>,
|}>;
export default (codegenNativeComponent<ModuleProps>(
'Module',
): HostComponent<ModuleProps>);
`;
const STATE_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 type ModuleNativeState = $ReadOnly<{|
isEnabled: string,
isEnabled: boolean,
|}>;
export default (codegenNativeComponent<ModuleProps>(
'Module',
): HostComponent<ModuleProps>);
`;
const STATE_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 StateInFile = $ReadOnly<{|
isEnabled: boolean,
|}>;
export type ModuleProps = $ReadOnly<{|
...ViewProps,
|}>;
export type ModuleNativeState = $ReadOnly<{|
...StateInFile,
isEnabled: boolean,
|}>;
export default (codegenNativeComponent<ModuleProps>(
'Module',
): HostComponent<ModuleProps>);
`;
const STATE_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 StateInFile = $ReadOnly<{|
isEnabled: boolean,
|}>;
export type ModuleProps = $ReadOnly<{|
...ViewProps,
|}>;
export type ModuleNativeState = $ReadOnly<{|
isEnabled: boolean,
...StateInFile,
|}>;
export default (codegenNativeComponent<ModuleProps>(
'Module',
): HostComponent<ModuleProps>);
`;
const STATE_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,
|}>;
export type ModuleNativeState = $ReadOnly<{|
someProp: number
|}>;
export default (codegenNativeComponent<ModuleProps>(
'Module',
): HostComponent<ModuleProps>);
`;
const STATE_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,
|}>;
export type ModuleNativeState = $ReadOnly<{|
someProp?: WithDefault<'foo' | 1, 1>
|}>;
export default (codegenNativeComponent<ModuleProps>(
'Module',
): HostComponent<ModuleProps>);
`;
const STATE_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,
|}>;
export type ModuleNativeState = $ReadOnly<{|
someProp?: WithDefault<false | true, false>
|}>;
export default (codegenNativeComponent<ModuleProps>(
'Module',
): HostComponent<ModuleProps>);
`;
const STATE_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,
|}>;
export type ModuleNativeState = $ReadOnly<{|
someProp?: WithDefault<$ReadOnlyArray<'foo' | 1>, 1>
|}>;
export default (codegenNativeComponent<ModuleProps>(
'Module',
): HostComponent<ModuleProps>);
`;
const STATE_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,
|}>;
export type ModuleNativeState = $ReadOnly<{|
someProp?: WithDefault<$ReadOnlyArray<false | true>, false>
|}>;
export default (codegenNativeComponent<ModuleProps>(
'Module',
): HostComponent<ModuleProps>);
`;
const STATE_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,
|}>;
export type ModuleNativeState = $ReadOnly<{|
someProp?: WithDefault<$ReadOnlyArray<0 | 1>, 0>
|}>;
export default (codegenNativeComponent<ModuleProps>(
'Module',
): HostComponent<ModuleProps>);
`;
const DOUBLE_STATE_IN_FILE = `
/**
* Copyright (c) Meta Platforms, Inc. and 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,
|}>;
type SecondNativeState = $ReadOnly<{|
someProp: boolean
|}>;
export type FirstNativeState = $ReadOnly<{|
someOtherProp: boolean
|}>;
export default (codegenNativeComponent<ModuleProps>(
'Module',
): HostComponent<ModuleProps>);
`;
module.exports = {
COMMANDS_DEFINED_INLINE,
COMMANDS_DEFINED_MULTIPLE_TIMES,
@@ -597,4 +993,16 @@ module.exports = {
PROP_ARRAY_MIXED_ENUM,
PROP_ARRAY_ENUM_BOOLEAN,
PROP_ARRAY_ENUM_INT,
NULLABLE_STATE_WITH_DEFAULT,
NON_OPTIONAL_KEY_STATE_WITH_DEFAULT_VALUE,
STATE_CONFLICT_NAMES,
STATE_CONFLICT_WITH_SPREAD_PROPS,
STATE_SPREAD_CONFLICTS_WITH_PROPS,
STATE_NUMBER_TYPE,
STATE_MIXED_ENUM,
STATE_ENUM_BOOLEAN,
STATE_ARRAY_MIXED_ENUM,
STATE_ARRAY_ENUM_BOOLEAN,
STATE_ARRAY_ENUM_INT,
DOUBLE_STATE_IN_FILE,
};
@@ -939,7 +939,7 @@ export default (codegenNativeComponent<ModuleProps>(
): NativeType);
`;
const COMMANDS_AND_EVENTS_TYPES_EXPORTED = `
const COMMANDS_EVENTS_STATE_TYPES_EXPORTED = `
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
@@ -981,6 +981,13 @@ export type ModuleProps = $ReadOnly<{|
onDirectEventDefinedInlineWithPaperName: DirectEventHandler<EventInFile, 'paperDirectEventDefinedInlineWithPaperName'>,
|}>;
// Add state here
export type ModuleNativeState = $ReadOnly<{|
boolean_required: boolean,
boolean_optional_key?: WithDefault<boolean, true>,
boolean_optional_both?: WithDefault<boolean, true>,
|}>
type NativeType = HostComponent<ModuleProps>;
export type ScrollTo = (viewRef: React.ElementRef<NativeType>, y: Int, animated: Boolean) => Void;
@@ -998,6 +1005,429 @@ export default (codegenNativeComponent<ModuleProps>(
): NativeType);
`;
// === STATE === //
const ALL_STATE_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 {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 ModuleProps = $ReadOnly<{|
...ViewProps,
|}>;
type ModuleNativeState = $ReadOnly<{|
// Boolean props
boolean_required: boolean,
boolean_optional_key?: WithDefault<boolean, true>,
boolean_optional_both?: WithDefault<boolean, true>,
// Boolean props, null default
boolean_null_optional_key?: WithDefault<boolean, null>,
boolean_null_optional_both?: WithDefault<boolean, null>,
// String props
string_required: string,
string_optional_key?: WithDefault<string, ''>,
string_optional_both?: WithDefault<string, ''>,
// String props, null default
string_null_optional_key?: WithDefault<string, null>,
string_null_optional_both?: WithDefault<string, null>,
// Stringish props
stringish_required: Stringish,
stringish_optional_key?: WithDefault<Stringish, ''>,
stringish_optional_both?: WithDefault<Stringish, ''>,
// Stringish props, null default
stringish_null_optional_key?: WithDefault<Stringish, null>,
stringish_null_optional_both?: WithDefault<Stringish, null>,
// Double props
double_required: Double,
double_optional_key?: WithDefault<Double, 1.1>,
double_optional_both?: WithDefault<Double, 1.1>,
// Float props
float_required: Float,
float_optional_key?: WithDefault<Float, 1.1>,
float_optional_both?: WithDefault<Float, 1.1>,
// Float props, null default
float_null_optional_key?: WithDefault<Float, null>,
float_null_optional_both?: WithDefault<Float, null>,
// Int32 props
int32_required: Int32,
int32_optional_key?: WithDefault<Int32, 1>,
int32_optional_both?: WithDefault<Int32, 1>,
// 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,
|}>;
export default (codegenNativeComponent<ModuleProps, Options>(
'Module',
): HostComponent<ModuleProps>);
`;
const ARRAY_STATE_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 {Int32, Double, Float, WithDefault} from 'CodegenTypes';
import type {ImageSource} from 'ImageSource';
import type {ColorValue, PointValue, ProcessColorValue, EdgeInsetsValue} 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,
|}>;
type ModuleNativeState = $ReadOnly<{|
// Props
// Boolean props
array_boolean_required: $ReadOnlyArray<boolean>,
array_boolean_optional_key?: $ReadOnlyArray<boolean>,
array_boolean_optional_value: ?$ReadOnlyArray<boolean>,
array_boolean_optional_both?: ?$ReadOnlyArray<boolean>,
// String props
array_string_required: $ReadOnlyArray<string>,
array_string_optional_key?: $ReadOnlyArray<string>,
array_string_optional_value: ?$ReadOnlyArray<string>,
array_string_optional_both?: ?$ReadOnlyArray<string>,
// Double props
array_double_required: $ReadOnlyArray<Double>,
array_double_optional_key?: $ReadOnlyArray<Double>,
array_double_optional_value: ?$ReadOnlyArray<Double>,
array_double_optional_both?: ?$ReadOnlyArray<Double>,
// Float props
array_float_required: $ReadOnlyArray<Float>,
array_float_optional_key?: $ReadOnlyArray<Float>,
array_float_optional_value: ?$ReadOnlyArray<Float>,
array_float_optional_both?: ?$ReadOnlyArray<Float>,
// Int32 props
array_int32_required: $ReadOnlyArray<Int32>,
array_int32_optional_key?: $ReadOnlyArray<Int32>,
array_int32_optional_value: ?$ReadOnlyArray<Int32>,
array_int32_optional_both?: ?$ReadOnlyArray<Int32>,
// 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<ImageSource>,
array_image_optional_key?: $ReadOnlyArray<ImageSource>,
array_image_optional_value: ?$ReadOnlyArray<ImageSource>,
array_image_optional_both?: ?$ReadOnlyArray<ImageSource>,
// ColorValue props
array_color_required: $ReadOnlyArray<ColorValue>,
array_color_optional_key?: $ReadOnlyArray<ColorValue>,
array_color_optional_value: ?$ReadOnlyArray<ColorValue>,
array_color_optional_both?: ?$ReadOnlyArray<ColorValue>,
// PointValue props
array_point_required: $ReadOnlyArray<PointValue>,
array_point_optional_key?: $ReadOnlyArray<PointValue>,
array_point_optional_value: ?$ReadOnlyArray<PointValue>,
array_point_optional_both?: ?$ReadOnlyArray<PointValue>,
// EdgeInsetsValue props
array_insets_required: $ReadOnlyArray<EdgeInsetsValue>,
array_insets_optional_key?: $ReadOnlyArray<EdgeInsetsValue>,
array_insets_optional_value: ?$ReadOnlyArray<EdgeInsetsValue>,
array_insets_optional_both?: ?$ReadOnlyArray<EdgeInsetsValue>,
// 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<ObjectType>,
// 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<ObjectType>,
>,
// Nested array of array of object types (with spread)
array_of_array_of_object_required_with_spread: $ReadOnlyArray<
$ReadOnlyArray<
$ReadOnly<{|
...ObjectType
|}>,
>,
>,
|}>;
export default (codegenNativeComponent<ModuleProps>(
'Module',
): HostComponent<ModuleProps>);
`;
const OBJECT_STATE_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 {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,
|}>;
type ModuleNativeState = $ReadOnly<{|
// Props
// Boolean props
boolean_required: $ReadOnly<{|prop: boolean|}>,
boolean_optional: $ReadOnly<{|prop?: WithDefault<boolean, false>|}>,
// String props
string_required: $ReadOnly<{|prop: string|}>,
string_optional: $ReadOnly<{|prop?: WithDefault<string, ''>|}>,
// Double props
double_required: $ReadOnly<{|prop: Double|}>,
double_optional: $ReadOnly<{|prop?: WithDefault<Double, 0.0>|}>,
// Float props
float_required: $ReadOnly<{|prop: Float|}>,
float_optional: $ReadOnly<{|prop?: WithDefault<Float, 0.0>|}>,
// Int32 props
int_required: $ReadOnly<{|prop: Int32|}>,
int_optional: $ReadOnly<{|prop?: WithDefault<Int32, 0>|}>,
// 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|}>,
// 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<ModuleProps>(
'Module',
): HostComponent<ModuleProps>);
`;
//TODO: fix this. The code is the same as per the props, but it fails with the State.
// const STATE_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 StateInFile = $ReadOnly<{|
// ...DeepSpread,
// isEnabled: boolean,
// label: string,
// |}>;
// export type ModuleProps = $ReadOnly<{|
// ...ViewProps,
// |}>;
// export type ModuleNativeState = $ReadOnly<{|
// ...StateInFile,
// localType: $ReadOnly<{|
// ...StateInFile
// |}>,
// localArr: $ReadOnlyArray<StateInFile>
// ||}>;
// export default (codegenNativeComponent<ModuleProps>(
// 'Module',
// ): HostComponent<ModuleProps>);
// `;
module.exports = {
ALL_PROP_TYPES_NO_EVENTS,
ARRAY_PROP_TYPES_NO_EVENTS,
@@ -1009,8 +1439,12 @@ module.exports = {
EVENTS_DEFINED_INLINE_WITH_ALL_TYPES,
EVENTS_DEFINED_AS_NULL_INLINE,
PROPS_AND_EVENTS_TYPES_EXPORTED,
COMMANDS_AND_EVENTS_TYPES_EXPORTED,
COMMANDS_EVENTS_STATE_TYPES_EXPORTED,
COMMANDS_DEFINED_WITH_ALL_TYPES,
PROPS_AS_EXTERNAL_TYPES,
COMMANDS_WITH_EXTERNAL_TYPES,
ALL_STATE_TYPES,
ARRAY_STATE_TYPES,
OBJECT_STATE_TYPES,
// STATE_ALIASED_LOCALLY,
};
@@ -0,0 +1,486 @@
/**
* Copyright (c) Meta Platforms, Inc. 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 {ASTNode} from '../utils';
import type {TypeDeclarationMap} from '../utils.js';
import type {NamedShape} from '../../../CodegenSchema.js';
const {getValueFromTypes} = require('../utils.js');
function 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`,
);
}
}
function getTypeAnnotationForArray<+T>(
name: string,
typeAnnotation: $FlowFixMe,
defaultValue: $FlowFixMe | null,
types: TypeDeclarationMap,
buildSchema: (property: PropAST, types: TypeDeclarationMap) => ?NamedShape<T>,
): $FlowFixMe {
const extractedTypeAnnotation = getValueFromTypes(typeAnnotation, types);
if (extractedTypeAnnotation.type === 'NullableTypeAnnotation') {
throw new Error(
'Nested optionals such as "$ReadOnlyArray<?boolean>" are not supported, please declare optionals at the top level of value definitions as in "?$ReadOnlyArray<boolean>"',
);
}
if (
extractedTypeAnnotation.type === 'GenericTypeAnnotation' &&
extractedTypeAnnotation.id.name === 'WithDefault'
) {
throw new Error(
'Nested defaults such as "$ReadOnlyArray<WithDefault<boolean, false>>" are not supported, please declare defaults at the top level of value definitions as in "WithDefault<$ReadOnlyArray<boolean>, false>"',
);
}
if (extractedTypeAnnotation.type === 'GenericTypeAnnotation') {
// Resolve the type alias if it's not defined inline
const objectType = getValueFromTypes(extractedTypeAnnotation, types);
if (objectType.id.name === '$ReadOnly') {
return {
type: 'ObjectTypeAnnotation',
properties: flattenProperties(
objectType.typeParameters.params[0].properties,
types,
)
.map(prop => buildSchema(prop, types))
.filter(Boolean),
};
}
if (objectType.id.name === '$ReadOnlyArray') {
// We need to go yet another level deeper to resolve
// types that may be defined in a type alias
const nestedObjectType = getValueFromTypes(
objectType.typeParameters.params[0],
types,
);
return {
type: 'ArrayTypeAnnotation',
elementType: {
type: 'ObjectTypeAnnotation',
properties: flattenProperties(
nestedObjectType.typeParameters.params[0].properties,
types,
)
.map(prop => buildSchema(prop, types))
.filter(Boolean),
},
};
}
}
const type =
extractedTypeAnnotation.type === 'GenericTypeAnnotation'
? extractedTypeAnnotation.id.name
: extractedTypeAnnotation.type;
switch (type) {
case 'ImageSource':
return {
type: 'ReservedPropTypeAnnotation',
name: 'ImageSourcePrimitive',
};
case 'ColorValue':
case 'ProcessedColorValue':
return {
type: 'ReservedPropTypeAnnotation',
name: 'ColorPrimitive',
};
case 'PointValue':
return {
type: 'ReservedPropTypeAnnotation',
name: 'PointPrimitive',
};
case 'EdgeInsetsValue':
return {
type: 'ReservedPropTypeAnnotation',
name: 'EdgeInsetsPrimitive',
};
case 'Stringish':
return {
type: 'StringTypeAnnotation',
};
case 'Int32':
return {
type: 'Int32TypeAnnotation',
};
case 'Double':
return {
type: 'DoubleTypeAnnotation',
};
case 'Float':
return {
type: 'FloatTypeAnnotation',
};
case 'BooleanTypeAnnotation':
return {
type: 'BooleanTypeAnnotation',
};
case 'StringTypeAnnotation':
return {
type: 'StringTypeAnnotation',
};
case 'UnionTypeAnnotation':
typeAnnotation.types.reduce((lastType, currType) => {
if (lastType && currType.type !== lastType.type) {
throw new Error(`Mixed types are not supported (see "${name}")`);
}
return currType;
});
if (defaultValue === null) {
throw new Error(`A default enum value is required for "${name}"`);
}
const unionType = typeAnnotation.types[0].type;
if (unionType === 'StringLiteralTypeAnnotation') {
return {
type: 'StringEnumTypeAnnotation',
default: (defaultValue: string),
options: typeAnnotation.types.map(option => option.value),
};
} else if (unionType === 'NumberLiteralTypeAnnotation') {
throw new Error(
`Arrays of int enums are not supported (see: "${name}")`,
);
} else {
throw new Error(
`Unsupported union type for "${name}", received "${unionType}"`,
);
}
default:
(type: empty);
throw new Error(`Unknown property type for "${name}": ${type}`);
}
}
function flattenProperties(
typeDefinition: $ReadOnlyArray<PropAST>,
types: TypeDeclarationMap,
): $ReadOnlyArray<PropAST> {
return typeDefinition
.map(property => {
if (property.type === 'ObjectTypeProperty') {
return property;
} else if (property.type === 'ObjectTypeSpreadProperty') {
return flattenProperties(
getProperties(property.argument.id.name, types),
types,
);
}
})
.reduce((acc, item) => {
if (Array.isArray(item)) {
item.forEach(prop => {
verifyPropNotAlreadyDefined(acc, prop);
});
return acc.concat(item);
} else {
verifyPropNotAlreadyDefined(acc, item);
acc.push(item);
return acc;
}
}, [])
.filter(Boolean);
}
function verifyPropNotAlreadyDefined(
props: $ReadOnlyArray<PropAST>,
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 getTypeAnnotation<+T>(
name: string,
annotation: $FlowFixMe | ASTNode,
defaultValue: $FlowFixMe | null,
withNullDefault: boolean,
types: TypeDeclarationMap,
buildSchema: (property: PropAST, types: TypeDeclarationMap) => ?NamedShape<T>,
): $FlowFixMe {
const typeAnnotation = getValueFromTypes(annotation, types);
if (
typeAnnotation.type === 'GenericTypeAnnotation' &&
typeAnnotation.id.name === '$ReadOnlyArray'
) {
return {
type: 'ArrayTypeAnnotation',
elementType: getTypeAnnotationForArray(
name,
typeAnnotation.typeParameters.params[0],
defaultValue,
types,
buildSchema,
),
};
}
if (
typeAnnotation.type === 'GenericTypeAnnotation' &&
typeAnnotation.id.name === '$ReadOnly'
) {
return {
type: 'ObjectTypeAnnotation',
properties: flattenProperties(
typeAnnotation.typeParameters.params[0].properties,
types,
)
.map(prop => buildSchema(prop, types))
.filter(Boolean),
};
}
const type =
typeAnnotation.type === 'GenericTypeAnnotation'
? typeAnnotation.id.name
: typeAnnotation.type;
switch (type) {
case 'ImageSource':
return {
type: 'ReservedPropTypeAnnotation',
name: 'ImageSourcePrimitive',
};
case 'ColorValue':
case 'ProcessedColorValue':
return {
type: 'ReservedPropTypeAnnotation',
name: 'ColorPrimitive',
};
case 'ColorArrayValue':
return {
type: 'ArrayTypeAnnotation',
elementType: {
type: 'ReservedPropTypeAnnotation',
name: 'ColorPrimitive',
},
};
case 'PointValue':
return {
type: 'ReservedPropTypeAnnotation',
name: 'PointPrimitive',
};
case 'EdgeInsetsValue':
return {
type: 'ReservedPropTypeAnnotation',
name: 'EdgeInsetsPrimitive',
};
case 'Int32':
return {
type: 'Int32TypeAnnotation',
default: ((defaultValue ? defaultValue : 0): number),
};
case 'Double':
return {
type: 'DoubleTypeAnnotation',
default: ((defaultValue ? defaultValue : 0): number),
};
case 'Float':
return {
type: 'FloatTypeAnnotation',
default: withNullDefault
? (defaultValue: number | null)
: ((defaultValue ? defaultValue : 0): number),
};
case 'BooleanTypeAnnotation':
return {
type: 'BooleanTypeAnnotation',
default: withNullDefault
? (defaultValue: boolean | null)
: ((defaultValue == null ? false : defaultValue): boolean),
};
case 'StringTypeAnnotation':
if (typeof defaultValue !== 'undefined') {
return {
type: 'StringTypeAnnotation',
default: (defaultValue: string | null),
};
}
throw new Error(`A default string (or null) is required for "${name}"`);
case 'Stringish':
if (typeof defaultValue !== 'undefined') {
return {
type: 'StringTypeAnnotation',
default: (defaultValue: string | null),
};
}
throw new Error(`A default string (or null) is required for "${name}"`);
case 'UnionTypeAnnotation':
typeAnnotation.types.reduce((lastType, currType) => {
if (lastType && currType.type !== lastType.type) {
throw new Error(`Mixed types are not supported (see "${name}").`);
}
return currType;
});
if (defaultValue === null) {
throw new Error(`A default enum value is required for "${name}"`);
}
const unionType = typeAnnotation.types[0].type;
if (unionType === 'StringLiteralTypeAnnotation') {
return {
type: 'StringEnumTypeAnnotation',
default: (defaultValue: string),
options: typeAnnotation.types.map(option => option.value),
};
} else if (unionType === 'NumberLiteralTypeAnnotation') {
return {
type: 'Int32EnumTypeAnnotation',
default: (defaultValue: number),
options: typeAnnotation.types.map(option => option.value),
};
} else {
throw new Error(
`Unsupported union type for "${name}", received "${unionType}"`,
);
}
case '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`,
);
default:
(type: empty);
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,
};
}
// $FlowFixMe[unclear-type] there's no flowtype for ASTs
type PropAST = Object;
module.exports = {
getProperties,
getSchemaInfo,
getTypeAnnotation,
flattenProperties,
};
@@ -16,9 +16,11 @@ import type {ComponentSchemaBuilderConfig} from './schema.js';
const {getTypes} = require('../utils');
const {getCommands} = require('./commands');
const {getEvents} = require('./events');
const {getState} = require('./states');
const {getExtendsProps, removeKnownExtends} = require('./extends');
const {getCommandOptions, getOptions} = require('./options');
const {getPropProperties, getProps} = require('./props');
const {getProps} = require('./props');
const {getProperties} = require('./componentsUtils.js');
/* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's
* LTI update could not be added via codemod */
@@ -111,8 +113,32 @@ function findComponentConfig(ast) {
throw new Error('codegenNativeCommands may only be called once in a file');
}
const unexportedStateTypes: Array<string> = ast.body
.filter(
node =>
node.type === 'TypeAlias' && node.id.name.indexOf('NativeState') >= 0,
)
.map(node => node.id.name);
const exportedStateTypes: Array<string> = namedExports
.filter(
node =>
node.declaration.id &&
node.declaration.id.name.indexOf('NativeState') >= 0,
)
.map(node => node.declaration.id.name);
const stateTypeName = exportedStateTypes.concat(unexportedStateTypes);
if (Array.isArray(stateTypeName) && stateTypeName.length > 1) {
throw new Error(
`Found ${stateTypeName.length} NativeStates for ${foundConfig.componentName}. Each component can have only 1 NativeState`,
);
}
return {
...foundConfig,
stateTypeName: stateTypeName.length === 1 ? stateTypeName[0] : '',
commandTypeName:
commandsTypeNames[0] == null
? null
@@ -185,6 +211,7 @@ function buildComponentSchema(ast): ComponentSchemaBuilderConfig {
const {
componentName,
propsTypeName,
stateTypeName,
commandTypeName,
commandOptionsExpression,
optionsExpression,
@@ -192,7 +219,7 @@ function buildComponentSchema(ast): ComponentSchemaBuilderConfig {
const types = getTypes(ast);
const propProperties = getPropProperties(propsTypeName, types);
const propProperties = getProperties(propsTypeName, types);
const commandOptions = getCommandOptions(commandOptionsExpression);
const commandProperties = getCommandProperties(
@@ -209,7 +236,7 @@ function buildComponentSchema(ast): ComponentSchemaBuilderConfig {
const events = getEvents(propProperties, types);
const commands = getCommands(commandProperties, types);
return {
const toRet = {
filename: componentName,
componentName,
options,
@@ -218,6 +245,17 @@ function buildComponentSchema(ast): ComponentSchemaBuilderConfig {
props,
commands,
};
if (stateTypeName) {
const stateProperties = getProperties(stateTypeName, types);
const state = getState(stateProperties, types);
return {
...toRet,
state,
};
}
return toRet;
}
module.exports = {
@@ -9,409 +9,28 @@
*/
'use strict';
import type {ASTNode} from '../utils';
const {getValueFromTypes} = require('../utils.js');
const {
flattenProperties,
getSchemaInfo,
getTypeAnnotation,
} = require('./componentsUtils.js');
import type {NamedShape, PropTypeAnnotation} from '../../../CodegenSchema.js';
import type {TypeDeclarationMap} from '../utils.js';
function getPropProperties(
propsTypeName: string,
types: TypeDeclarationMap,
): $FlowFixMe {
const typeAlias = types[propsTypeName];
try {
return typeAlias.right.typeParameters.params[0].properties;
} catch (e) {
throw new Error(
`Failed to find type definition for "${propsTypeName}", please check that you have a valid codegen flow file`,
);
}
}
function getTypeAnnotationForArray(
name: string,
typeAnnotation: $FlowFixMe,
defaultValue: $FlowFixMe | null,
types: TypeDeclarationMap,
) {
const extractedTypeAnnotation = getValueFromTypes(typeAnnotation, types);
if (extractedTypeAnnotation.type === 'NullableTypeAnnotation') {
throw new Error(
'Nested optionals such as "$ReadOnlyArray<?boolean>" are not supported, please declare optionals at the top level of value definitions as in "?$ReadOnlyArray<boolean>"',
);
}
if (
extractedTypeAnnotation.type === 'GenericTypeAnnotation' &&
extractedTypeAnnotation.id.name === 'WithDefault'
) {
throw new Error(
'Nested defaults such as "$ReadOnlyArray<WithDefault<boolean, false>>" are not supported, please declare defaults at the top level of value definitions as in "WithDefault<$ReadOnlyArray<boolean>, false>"',
);
}
if (extractedTypeAnnotation.type === 'GenericTypeAnnotation') {
// Resolve the type alias if it's not defined inline
const objectType = getValueFromTypes(extractedTypeAnnotation, types);
if (objectType.id.name === '$ReadOnly') {
return {
type: 'ObjectTypeAnnotation',
properties: flattenProperties(
objectType.typeParameters.params[0].properties,
types,
)
.map(prop => buildPropSchema(prop, types))
.filter(Boolean),
};
}
if (objectType.id.name === '$ReadOnlyArray') {
// We need to go yet another level deeper to resolve
// types that may be defined in a type alias
const nestedObjectType = getValueFromTypes(
objectType.typeParameters.params[0],
types,
);
return {
type: 'ArrayTypeAnnotation',
elementType: {
type: 'ObjectTypeAnnotation',
properties: flattenProperties(
nestedObjectType.typeParameters.params[0].properties,
types,
)
.map(prop => buildPropSchema(prop, types))
.filter(Boolean),
},
};
}
}
const type =
extractedTypeAnnotation.type === 'GenericTypeAnnotation'
? extractedTypeAnnotation.id.name
: extractedTypeAnnotation.type;
switch (type) {
case 'ImageSource':
return {
type: 'ReservedPropTypeAnnotation',
name: 'ImageSourcePrimitive',
};
case 'ColorValue':
case 'ProcessedColorValue':
return {
type: 'ReservedPropTypeAnnotation',
name: 'ColorPrimitive',
};
case 'PointValue':
return {
type: 'ReservedPropTypeAnnotation',
name: 'PointPrimitive',
};
case 'EdgeInsetsValue':
return {
type: 'ReservedPropTypeAnnotation',
name: 'EdgeInsetsPrimitive',
};
case 'Stringish':
return {
type: 'StringTypeAnnotation',
};
case 'Int32':
return {
type: 'Int32TypeAnnotation',
};
case 'Double':
return {
type: 'DoubleTypeAnnotation',
};
case 'Float':
return {
type: 'FloatTypeAnnotation',
};
case 'BooleanTypeAnnotation':
return {
type: 'BooleanTypeAnnotation',
};
case 'StringTypeAnnotation':
return {
type: 'StringTypeAnnotation',
};
case 'UnionTypeAnnotation':
typeAnnotation.types.reduce((lastType, currType) => {
if (lastType && currType.type !== lastType.type) {
throw new Error(`Mixed types are not supported (see "${name}")`);
}
return currType;
});
if (defaultValue === null) {
throw new Error(`A default enum value is required for "${name}"`);
}
const unionType = typeAnnotation.types[0].type;
if (unionType === 'StringLiteralTypeAnnotation') {
return {
type: 'StringEnumTypeAnnotation',
default: (defaultValue: string),
options: typeAnnotation.types.map(option => option.value),
};
} else if (unionType === 'NumberLiteralTypeAnnotation') {
throw new Error(
`Arrays of int enums are not supported (see: "${name}")`,
);
} else {
throw new Error(
`Unsupported union type for "${name}", received "${unionType}"`,
);
}
default:
(type: empty);
throw new Error(`Unknown prop type for "${name}": ${type}`);
}
}
function getTypeAnnotation(
name: string,
annotation: $FlowFixMe | ASTNode,
defaultValue: $FlowFixMe | null,
withNullDefault: boolean,
types: TypeDeclarationMap,
) {
const typeAnnotation = getValueFromTypes(annotation, types);
if (
typeAnnotation.type === 'GenericTypeAnnotation' &&
typeAnnotation.id.name === '$ReadOnlyArray'
) {
return {
type: 'ArrayTypeAnnotation',
elementType: getTypeAnnotationForArray(
name,
typeAnnotation.typeParameters.params[0],
defaultValue,
types,
),
};
}
if (
typeAnnotation.type === 'GenericTypeAnnotation' &&
typeAnnotation.id.name === '$ReadOnly'
) {
return {
type: 'ObjectTypeAnnotation',
properties: flattenProperties(
typeAnnotation.typeParameters.params[0].properties,
types,
)
.map(prop => buildPropSchema(prop, types))
.filter(Boolean),
};
}
const type =
typeAnnotation.type === 'GenericTypeAnnotation'
? typeAnnotation.id.name
: typeAnnotation.type;
switch (type) {
case 'ImageSource':
return {
type: 'ReservedPropTypeAnnotation',
name: 'ImageSourcePrimitive',
};
case 'ColorValue':
case 'ProcessedColorValue':
return {
type: 'ReservedPropTypeAnnotation',
name: 'ColorPrimitive',
};
case 'ColorArrayValue':
return {
type: 'ArrayTypeAnnotation',
elementType: {
type: 'ReservedPropTypeAnnotation',
name: 'ColorPrimitive',
},
};
case 'PointValue':
return {
type: 'ReservedPropTypeAnnotation',
name: 'PointPrimitive',
};
case 'EdgeInsetsValue':
return {
type: 'ReservedPropTypeAnnotation',
name: 'EdgeInsetsPrimitive',
};
case 'Int32':
return {
type: 'Int32TypeAnnotation',
default: ((defaultValue ? defaultValue : 0): number),
};
case 'Double':
return {
type: 'DoubleTypeAnnotation',
default: ((defaultValue ? defaultValue : 0): number),
};
case 'Float':
return {
type: 'FloatTypeAnnotation',
default: withNullDefault
? (defaultValue: number | null)
: ((defaultValue ? defaultValue : 0): number),
};
case 'BooleanTypeAnnotation':
return {
type: 'BooleanTypeAnnotation',
default: withNullDefault
? (defaultValue: boolean | null)
: ((defaultValue == null ? false : defaultValue): boolean),
};
case 'StringTypeAnnotation':
if (typeof defaultValue !== 'undefined') {
return {
type: 'StringTypeAnnotation',
default: (defaultValue: string | null),
};
}
throw new Error(`A default string (or null) is required for "${name}"`);
case 'Stringish':
if (typeof defaultValue !== 'undefined') {
return {
type: 'StringTypeAnnotation',
default: (defaultValue: string | null),
};
}
throw new Error(`A default string (or null) is required for "${name}"`);
case 'UnionTypeAnnotation':
typeAnnotation.types.reduce((lastType, currType) => {
if (lastType && currType.type !== lastType.type) {
throw new Error(`Mixed types are not supported (see "${name}")`);
}
return currType;
});
if (defaultValue === null) {
throw new Error(`A default enum value is required for "${name}"`);
}
const unionType = typeAnnotation.types[0].type;
if (unionType === 'StringLiteralTypeAnnotation') {
return {
type: 'StringEnumTypeAnnotation',
default: (defaultValue: string),
options: typeAnnotation.types.map(option => option.value),
};
} else if (unionType === 'NumberLiteralTypeAnnotation') {
return {
type: 'Int32EnumTypeAnnotation',
default: (defaultValue: number),
options: typeAnnotation.types.map(option => option.value),
};
} else {
throw new Error(
`Unsupported union type for "${name}", received "${unionType}"`,
);
}
case '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`,
);
default:
(type: empty);
throw new Error(`Unknown prop type for "${name}": "${type}"`);
}
}
// $FlowFixMe[unclear-type] there's no flowtype for ASTs
type PropAST = Object;
function buildPropSchema(
property: PropAST,
types: TypeDeclarationMap,
): ?NamedShape<PropTypeAnnotation> {
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')
) {
const info = getSchemaInfo(property, types);
if (info == null) {
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;
}
}
const {name, optional, typeAnnotation, defaultValue, withNullDefault} = info;
return {
name,
@@ -422,54 +41,11 @@ function buildPropSchema(
defaultValue,
withNullDefault,
types,
buildPropSchema,
),
};
}
// $FlowFixMe[unclear-type] there's no flowtype for ASTs
type PropAST = Object;
function verifyPropNotAlreadyDefined(
props: $ReadOnlyArray<PropAST>,
needleProp: PropAST,
) {
const propName = needleProp.key.name;
const foundProp = props.some(prop => prop.key.name === propName);
if (foundProp) {
throw new Error(`A prop was already defined with the name ${propName}`);
}
}
function flattenProperties(
typeDefinition: $ReadOnlyArray<PropAST>,
types: TypeDeclarationMap,
) {
return typeDefinition
.map(property => {
if (property.type === 'ObjectTypeProperty') {
return property;
} else if (property.type === 'ObjectTypeSpreadProperty') {
return flattenProperties(
getPropProperties(property.argument.id.name, types),
types,
);
}
})
.reduce((acc, item) => {
if (Array.isArray(item)) {
item.forEach(prop => {
verifyPropNotAlreadyDefined(acc, prop);
});
return acc.concat(item);
} else {
verifyPropNotAlreadyDefined(acc, item);
acc.push(item);
return acc;
}
}, [])
.filter(Boolean);
}
function getProps(
typeDefinition: $ReadOnlyArray<PropAST>,
types: TypeDeclarationMap,
@@ -481,5 +57,4 @@ function getProps(
module.exports = {
getProps,
getPropProperties,
};
@@ -15,6 +15,7 @@ import type {
NamedShape,
CommandTypeAnnotation,
PropTypeAnnotation,
StateTypeAnnotation,
ExtendsPropsShape,
SchemaType,
OptionsShape,
@@ -27,6 +28,7 @@ export type ComponentSchemaBuilderConfig = $ReadOnly<{
events: $ReadOnlyArray<EventTypeShape>,
props: $ReadOnlyArray<NamedShape<PropTypeAnnotation>>,
commands: $ReadOnlyArray<NamedShape<CommandTypeAnnotation>>,
state?: $ReadOnlyArray<NamedShape<StateTypeAnnotation>>,
options?: ?OptionsShape,
}>;
@@ -36,6 +38,7 @@ function wrapComponentSchema({
extendsProps,
events,
props,
state,
options,
commands,
}: ComponentSchemaBuilderConfig): SchemaType {
@@ -50,6 +53,7 @@ function wrapComponentSchema({
events,
props,
commands,
state,
},
},
},
@@ -0,0 +1,60 @@
/**
* Copyright (c) Meta Platforms, Inc. 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 {
flattenProperties,
getSchemaInfo,
getTypeAnnotation,
} = require('./componentsUtils.js');
import type {StateTypeAnnotation, NamedShape} from '../../../CodegenSchema.js';
import type {TypeDeclarationMap} from '../utils.js';
// $FlowFixMe[unclear-type] there's no flowtype for ASTs
type PropAST = Object;
function buildStateSchema(
property: PropAST,
types: TypeDeclarationMap,
): ?NamedShape<StateTypeAnnotation> {
const info = getSchemaInfo(property, types);
if (info == null) {
return null;
}
const {name, optional, typeAnnotation, defaultValue, withNullDefault} = info;
return {
name,
optional,
typeAnnotation: getTypeAnnotation(
name,
typeAnnotation,
defaultValue,
withNullDefault,
types,
buildStateSchema,
),
};
}
function getState(
typeDefinition: $ReadOnlyArray<PropAST>,
types: TypeDeclarationMap,
): $ReadOnlyArray<NamedShape<StateTypeAnnotation>> {
return flattenProperties(typeDefinition, types)
.map(property => buildStateSchema(property, types))
.filter(Boolean);
}
module.exports = {
getState,
};