Parse custom NativeState in TypeScript (#34786)

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

This diff is the TS equivalent of D39686251.
It introduces the possibility to parse a custom Native State in Typescript.

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.

Currently, we only support using `interface` for the state and the interface must contain the `NativeState` string in its name.

This diff introduces also tests for the TypeScript parser and it aligns the tests between Flow and TS.

## Changelog

[General][Added] - Implement custom Native State parsing in TypeScript

Reviewed By: cortinico

Differential Revision: D39811476

fbshipit-source-id: 1e1b86b50b9632c13157ff6c8115f5ebcbada643
This commit is contained in:
Riccardo Cipolleschi
2022-09-26 07:33:07 -07:00
committed by Facebook GitHub Bot
parent 925b15351f
commit bbb2fb212d
9 changed files with 4346 additions and 745 deletions
@@ -14,20 +14,11 @@ 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 = [
//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 flowExtraCases = [];
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'].concat([
//TODO: remove these once we implement TypeScript parser for Custom State
'COMMANDS_AND_EVENTS_TYPES_EXPORTED',
]);
const ignoredCases = ['ARRAY_PROP_TYPES_NO_EVENTS'];
const tsExtraCases = ['ARRAY2_PROP_TYPES_NO_EVENTS', 'ARRAY2_STATE_TYPES'];
const ignoredCases = ['ARRAY_PROP_TYPES_NO_EVENTS', 'ARRAY_STATE_TYPES'];
compareSnaps(
flowFixtures,
@@ -485,6 +485,310 @@ export default codegenNativeComponent<ModuleProps>(
) as 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
*/
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 { }
interface ModuleNativeState {
nullable_with_default: WithDefault<Float, 1.0> | null | undefined;
}
export default codegenNativeComponent<ModuleProps>(
'Module',
) as 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
*/
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 {}
export interface ModuleNativeState {
required_key_with_default: WithDefault<Float, 1.0>;
}
export default codegenNativeComponent<ModuleProps>(
'Module',
) as 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
*/
const codegenNativeComponent = require('codegenNativeComponent');
import type {ViewProps} from 'ViewPropTypes';
import type {HostComponent} from 'react-native';
export interface ModuleProps extends ViewProps { }
interface ModuleNativeState {
isEnabled: string,
isEnabled: boolean,
}
export default codegenNativeComponent<ModuleProps>(
'Module',
) as 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
*/
const codegenNativeComponent = require('codegenNativeComponent');
import type {ViewProps} from 'ViewPropTypes';
import type {HostComponent} from 'react-native';
export interface ModuleProps extends ViewProps { }
type PropsInFile = Readonly<{
isEnabled: boolean,
}>;
export interface ModuleNativeState extends PropsInFile {
isEnabled: boolean,
}
export default codegenNativeComponent<ModuleProps>(
'Module',
) as 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
*/
const codegenNativeComponent = require('codegenNativeComponent');
import type {ViewProps} from 'ViewPropTypes';
import type {HostComponent} from 'react-native';
export interface ModuleProps extends ViewProps { }
interface ModuleNativeState {
someProp: number
}
export default codegenNativeComponent<ModuleProps>(
'Module',
) as 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
*/
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 { }
export interface ModuleNativeState {
someProp?: WithDefault<'foo' | 1, 1>;
}
export default codegenNativeComponent<ModuleProps>(
'Module',
) as 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
*/
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 { }
interface ModuleNativeState {
someProp?: WithDefault<false | true, false>
}
export default codegenNativeComponent<ModuleProps>(
'Module',
) as 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
*/
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 { }
export interface ModuleNativeState {
someProp?: WithDefault<ReadonlyArray<'foo' | 1>, 1>;
}
export default codegenNativeComponent<ModuleProps>(
'Module',
) as 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
*/
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 { }
interface ModuleNativeState {
someProp?: WithDefault<ReadonlyArray<false | true>, false>;
}
export default codegenNativeComponent<ModuleProps>(
'Module',
) as 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
*/
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 { }
export interface ModuleNativeState {
someProp?: WithDefault<ReadonlyArray<0 | 1>, 0>;
}
export default codegenNativeComponent<ModuleProps>(
'Module',
) as 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
*/
const codegenNativeComponent = require('codegenNativeComponent');
import type {ViewProps} from 'ViewPropTypes';
import type {HostComponent} from 'react-native';
export interface ModuleProps extends ViewProps { }
interface SecondNativeState {
someProp: boolean
}
export interface ModuleNativeState {
someOtherProp: boolean
}
export default codegenNativeComponent<ModuleProps>(
'Module',
) as HostComponent<ModuleProps>;
`;
module.exports = {
COMMANDS_DEFINED_INLINE,
COMMANDS_DEFINED_MULTIPLE_TIMES,
@@ -502,4 +806,15 @@ 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_NUMBER_TYPE,
STATE_MIXED_ENUM,
STATE_ENUM_BOOLEAN,
STATE_ARRAY_MIXED_ENUM,
STATE_ARRAY_ENUM_BOOLEAN,
STATE_ARRAY_ENUM_INT,
DOUBLE_STATE_IN_FILE,
};
@@ -1034,7 +1034,7 @@ export default codegenNativeComponent<ModuleProps>('Module') as NativeType;
`;
const COMMANDS_AND_EVENTS_TYPES_EXPORTED = `
const COMMANDS_EVENTS_STATE_TYPES_EXPORTED = `
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
@@ -1071,6 +1071,13 @@ export interface ModuleProps extends ViewProps {
onDirectEventDefinedInlineWithPaperName: DirectEventHandler<EventInFile, 'paperDirectEventDefinedInlineWithPaperName'>,
}
// Add state here
export interface ModuleNativeState {
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;
@@ -1088,6 +1095,525 @@ export default codegenNativeComponent<ModuleProps>(
) as 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
*/
'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';
export interface ModuleProps extends ViewProps { }
export interface ModuleNativeState {
// Props
// 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} | 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;
}
export default codegenNativeComponent<ModuleProps>(
'Module',
) as 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
*/
'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 = ReadonlyArray<Readonly<{prop: string}>>;
export interface ModuleProps extends ViewProps {}
interface ModuleNativeState {
// Props
// Boolean props
array_boolean_required: ReadonlyArray<boolean>;
array_boolean_optional_key?: ReadonlyArray<boolean>;
array_boolean_optional_value: ReadonlyArray<boolean> | null | undefined;
array_boolean_optional_both?: ReadonlyArray<boolean> | null | undefined;
// String props
array_string_required: ReadonlyArray<string>;
array_string_optional_key?: ReadonlyArray<string>;
array_string_optional_value: ReadonlyArray<string> | null | undefined;
array_string_optional_both?: ReadonlyArray<string> | null | undefined;
// Double props
array_double_required: ReadonlyArray<Double>;
array_double_optional_key?: ReadonlyArray<Double>;
array_double_optional_value: ReadonlyArray<Double> | null | undefined;
array_double_optional_both?: ReadonlyArray<Double> | null | undefined;
// Float props
array_float_required: ReadonlyArray<Float>;
array_float_optional_key?: ReadonlyArray<Float>;
array_float_optional_value: ReadonlyArray<Float> | null | undefined;
array_float_optional_both?: ReadonlyArray<Float> | null | undefined;
// Int32 props
array_int32_required: ReadonlyArray<Int32>;
array_int32_optional_key?: ReadonlyArray<Int32>;
array_int32_optional_value: ReadonlyArray<Int32> | null | undefined;
array_int32_optional_both?: ReadonlyArray<Int32> | 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<ImageSource>;
array_image_optional_key?: ReadonlyArray<ImageSource>;
array_image_optional_value: ReadonlyArray<ImageSource> | null | undefined;
array_image_optional_both?: ReadonlyArray<ImageSource> | null | undefined;
// ColorValue props
array_color_required: ReadonlyArray<ColorValue>;
array_color_optional_key?: ReadonlyArray<ColorValue>;
array_color_optional_value: ReadonlyArray<ColorValue> | null | undefined;
array_color_optional_both?: ReadonlyArray<ColorValue> | null | undefined;
// PointValue props
array_point_required: ReadonlyArray<PointValue>;
array_point_optional_key?: ReadonlyArray<PointValue>;
array_point_optional_value: ReadonlyArray<PointValue> | null | undefined;
array_point_optional_both?: ReadonlyArray<PointValue> | null | undefined;
// EdgeInsetsValue props
array_insets_required: ReadonlyArray<EdgeInsetsValue>;
array_insets_optional_key?: ReadonlyArray<EdgeInsetsValue>;
array_insets_optional_value: ReadonlyArray<EdgeInsetsValue> | null | undefined;
array_insets_optional_both?: ReadonlyArray<EdgeInsetsValue> | null | undefined;
// Object props
array_object_required: ReadonlyArray<Readonly<{prop: string}>>;
array_object_optional_key?: ReadonlyArray<Readonly<{prop: string}>>;
array_object_optional_value: ArrayObjectType | null | undefined;
array_object_optional_both?: ReadonlyArray<ObjectType> | 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<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 | 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<ObjectType>
>;
}
export default codegenNativeComponent<ModuleProps>(
'Module',
) as HostComponent<ModuleProps>;
`;
const ARRAY2_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
*/
'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 {}
export interface ModuleNativeState{
// 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[][];
}
export default codegenNativeComponent<ModuleProps>(
'Module',
) as 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
*/
'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';
export interface ModuleProps extends ViewProps {}
interface ModuleNativeState {
// 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 | 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}>;
// 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<ModuleProps>(
'Module',
) as HostComponent<ModuleProps>;
`;
module.exports = {
ALL_PROP_TYPES_NO_EVENTS,
ARRAY_PROP_TYPES_NO_EVENTS,
@@ -1100,8 +1626,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,
ARRAY2_STATE_TYPES,
OBJECT_STATE_TYPES,
};
@@ -0,0 +1,693 @@
/**
* Copyright (c) Meta Platforms, Inc. 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 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`,
);
}
}
function getTypeAnnotationForObjectAsArrayElement<T>(
objectType: $FlowFixMe,
types: TypeDeclarationMap,
buildSchema: (property: PropAST, types: TypeDeclarationMap) => ?NamedShape<T>,
): $FlowFixMe {
return {
type: 'ObjectTypeAnnotation',
properties: flattenProperties(
objectType.typeParameters.params[0].members ||
objectType.typeParameters.params,
types,
)
.map(prop => buildSchema(prop, types))
.filter(Boolean),
};
}
function getTypeAnnotationForArrayOfArrayOfObject<T>(
typeAnnotation: $FlowFixMe,
types: TypeDeclarationMap,
buildSchema: (property: PropAST, types: TypeDeclarationMap) => ?NamedShape<T>,
): $FlowFixMe {
// We need to go yet another level deeper to resolve
// types that may be defined in a type alias
const nestedObjectType = getValueFromTypes(typeAnnotation, types);
return {
type: 'ArrayTypeAnnotation',
elementType: getTypeAnnotationForObjectAsArrayElement(
nestedObjectType,
types,
buildSchema,
),
};
}
function getTypeAnnotationForArray<T>(
name: string,
typeAnnotation: $FlowFixMe,
defaultValue: $FlowFixMe | null,
types: TypeDeclarationMap,
buildSchema: (property: PropAST, types: TypeDeclarationMap) => ?NamedShape<T>,
): $FlowFixMe {
if (typeAnnotation.type === 'TSParenthesizedType') {
return getTypeAnnotationForArray(
name,
typeAnnotation.typeAnnotation,
defaultValue,
types,
buildSchema,
);
}
const extractedTypeAnnotation = getValueFromTypes(typeAnnotation, types);
if (
extractedTypeAnnotation.type === 'TSUnionType' &&
extractedTypeAnnotation.types.some(
t => t.type === 'TSNullKeyword' || t.type === 'TSUndefinedKeyword',
)
) {
throw new Error(
'Nested optionals such as "ReadonlyArray<boolean | null | undefined>" are not supported, please declare optionals at the top level of value definitions as in "ReadonlyArray<boolean> | null | undefined"',
);
}
if (
extractedTypeAnnotation.type === 'TSTypeReference' &&
extractedTypeAnnotation.typeName.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>"',
);
}
// Covers: T[]
if (typeAnnotation.type === 'TSArrayType') {
return getTypeAnnotationForArrayOfArrayOfObject(
typeAnnotation.elementType,
types,
buildSchema,
);
}
if (extractedTypeAnnotation.type === 'TSTypeReference') {
// Resolve the type alias if it's not defined inline
const objectType = getValueFromTypes(extractedTypeAnnotation, types);
if (objectType.typeName.name === 'Readonly') {
return getTypeAnnotationForObjectAsArrayElement(
objectType,
types,
buildSchema,
);
}
// Covers: ReadonlyArray<T>
if (objectType.typeName.name === 'ReadonlyArray') {
return getTypeAnnotationForArrayOfArrayOfObject(
objectType.typeParameters.params[0],
types,
buildSchema,
);
}
}
const type =
extractedTypeAnnotation.elementType === 'TSTypeReference'
? extractedTypeAnnotation.elementType.typeName.name
: extractedTypeAnnotation.elementType?.type ||
extractedTypeAnnotation.typeName?.name ||
extractedTypeAnnotation.type;
switch (type) {
case 'TSNumberKeyword':
return {
type: 'FloatTypeAnnotation',
};
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 'TSBooleanKeyword':
return {
type: 'BooleanTypeAnnotation',
};
case 'TSStringKeyword':
return {
type: 'StringTypeAnnotation',
};
case 'TSUnionType':
typeAnnotation.types.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 === null) {
throw new Error(`A default enum value is required for "${name}"`);
}
const unionType = typeAnnotation.types[0].type;
if (
unionType === 'TSLiteralType' &&
typeAnnotation.types[0].literal?.type === 'StringLiteral'
) {
return {
type: 'StringEnumTypeAnnotation',
default: (defaultValue: string),
options: typeAnnotation.types.map(option => option.literal.value),
};
} else if (
unionType === 'TSLiteralType' &&
typeAnnotation.types[0].literal?.type === 'NumericLiteral'
) {
throw new Error(
`Arrays of int enums are not supported (see: "${name}")`,
);
} else {
throw new Error(
`Unsupported union type for "${name}", received "${
unionType === 'TSLiteralType'
? typeAnnotation.types[0].literal?.type
: unionType
}"`,
);
}
default:
(type: empty);
throw new Error(`Unknown prop type for "${name}": ${type}`);
}
}
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);
// Covers: (T)
if (typeAnnotation.type === 'TSParenthesizedType') {
return getTypeAnnotation(
name,
typeAnnotation.typeAnnotation,
defaultValue,
withNullDefault,
types,
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,
buildSchema,
),
};
}
// Covers: ReadonlyArray<T>
if (
typeAnnotation.type === 'TSTypeReference' &&
typeAnnotation.typeName.name === 'ReadonlyArray'
) {
return {
type: 'ArrayTypeAnnotation',
elementType: getTypeAnnotationForArray(
name,
typeAnnotation.typeParameters.params[0],
defaultValue,
types,
buildSchema,
),
};
}
// Covers: Readonly<T[]>
if (
typeAnnotation.type === 'TSTypeReference' &&
typeAnnotation.typeName?.name === 'Readonly' &&
typeAnnotation.typeParameters.type === 'TSTypeParameterInstantiation' &&
typeAnnotation.typeParameters.params[0].type === 'TSArrayType'
) {
return {
type: 'ArrayTypeAnnotation',
elementType: getTypeAnnotationForArray(
name,
typeAnnotation.typeParameters.params[0],
defaultValue,
types,
buildSchema,
),
};
}
if (
(typeAnnotation.type === 'TSTypeReference' ||
typeAnnotation.type === 'TSTypeLiteral') &&
typeAnnotation.typeName?.name === 'Readonly'
) {
const rawProperties =
typeAnnotation.typeParameters.params[0].members ||
(typeAnnotation.typeParameters.params[0].types &&
typeAnnotation.typeParameters.params[0].types[0].members) ||
typeAnnotation.typeParameters.params;
const flattenedProperties = flattenProperties(rawProperties, types);
const properties = flattenedProperties
.map(prop => buildSchema(prop, types))
.filter(Boolean);
return {
type: 'ObjectTypeAnnotation',
properties,
};
}
const type =
typeAnnotation.type === 'TSTypeReference' ||
typeAnnotation.type === 'TSTypeAliasDeclaration'
? typeAnnotation.typeName.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 'TSBooleanKeyword':
return {
type: 'BooleanTypeAnnotation',
default: withNullDefault
? (defaultValue: boolean | null)
: ((defaultValue == null ? false : defaultValue): boolean),
};
case 'TSStringKeyword':
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 'TSUnionType':
typeAnnotation.types.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 === null) {
throw new Error(`A default enum value is required for "${name}"`);
}
const unionType = typeAnnotation.types[0].type;
if (
unionType === 'TSLiteralType' &&
typeAnnotation.types[0].literal?.type === 'StringLiteral'
) {
return {
type: 'StringEnumTypeAnnotation',
default: (defaultValue: string),
options: typeAnnotation.types.map(option => option.literal.value),
};
} else if (
unionType === 'TSLiteralType' &&
typeAnnotation.types[0].literal?.type === 'NumericLiteral'
) {
return {
type: 'Int32EnumTypeAnnotation',
default: (defaultValue: number),
options: typeAnnotation.types.map(option => option.literal.value),
};
} else {
throw new Error(
`Unsupported union type for "${name}", received "${
unionType === 'TSLiteralType'
? typeAnnotation.types[0].literal?.type
: unionType
}"`,
);
}
case 'TSNumberKeyword':
throw new Error(
`Cannot use "${type}" type annotation for "${name}": must use a specific numeric type like Int32, Double, or Float`,
);
default:
(type: empty);
throw new Error(`Unknown prop type for "${name}": "${type}"`);
}
}
function findProp(
name: string,
typeAnnotation: $FlowFixMe,
optionalType: boolean,
) {
switch (typeAnnotation.type) {
// Check for (T)
case 'TSParenthesizedType':
return findProp(name, typeAnnotation.typeAnnotation, optionalType);
// Check for optional type in union e.g. T | null | undefined
case 'TSUnionType':
return findProp(
name,
typeAnnotation.types.filter(
t => t.type !== 'TSNullKeyword' && t.type !== 'TSUndefinedKeyword',
)[0],
optionalType ||
typeAnnotation.types.some(
t => t.type === 'TSNullKeyword' || t.type === 'TSUndefinedKeyword',
),
);
case 'TSTypeReference':
// Check against optional type inside `WithDefault`
if (typeAnnotation.typeName.name === 'WithDefault' && optionalType) {
throw new Error(
'WithDefault<> is optional and does not need to be marked as optional. Please remove the union of undefined and/or null',
);
}
// Remove unwanted types
if (
typeAnnotation.typeName.name === 'DirectEventHandler' ||
typeAnnotation.typeName.name === 'BubblingEventHandler'
) {
return null;
}
if (
name === 'style' &&
typeAnnotation.type === 'GenericTypeAnnotation' &&
typeAnnotation.typeName.name === 'ViewStyleProp'
) {
return null;
}
return {typeAnnotation, optionalType};
default:
return {typeAnnotation, optionalType};
}
}
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.typeAnnotation.typeAnnotation,
types,
);
const foundProp = findProp(name, value, false);
if (!foundProp) {
return null;
}
let {typeAnnotation, optionalType} = foundProp;
let optional = property.optional || optionalType;
// example: Readonly<{prop: string} | null | undefined>;
if (
value.type === 'TSTypeReference' &&
typeAnnotation.typeParameters?.params[0].type === 'TSUnionType' &&
typeAnnotation.typeParameters?.params[0].types.some(
element =>
element.type === 'TSNullKeyword' ||
element.type === 'TSUndefinedKeyword',
)
) {
optional = true;
}
if (
!property.optional &&
value.type === 'TSTypeReference' &&
typeAnnotation.typeName.name === 'WithDefault'
) {
throw new Error(
`key ${name} must be optional if used with WithDefault<> annotation`,
);
}
let type = typeAnnotation.type;
let defaultValue = null;
let withNullDefault = false;
if (
type === 'TSTypeReference' &&
typeAnnotation.typeName.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}"?`,
);
}
let defaultValueType = typeAnnotation.typeParameters.params[1].type;
defaultValue = typeAnnotation.typeParameters.params[1].value;
if (defaultValueType === 'TSLiteralType') {
defaultValueType = typeAnnotation.typeParameters.params[1].literal.type;
defaultValue = typeAnnotation.typeParameters.params[1].literal.value;
}
if (defaultValueType === 'TSNullKeyword') {
defaultValue = null;
withNullDefault = true;
}
typeAnnotation = typeAnnotation.typeParameters.params[0];
type =
typeAnnotation.type === 'TSTypeReference'
? typeAnnotation.typeName.name
: typeAnnotation.type;
}
return {
name,
optional,
typeAnnotation,
defaultValue,
withNullDefault,
};
}
// $FlowFixMe[unclear-type] TODO(T108222691): Use flow-types for @babel/parser
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,
): $ReadOnlyArray<PropAST> {
return typeDefinition
.map(property => {
if (property.type === 'TSPropertySignature') {
return property;
} else if (property.type === 'TSTypeReference') {
return flattenProperties(
getProperties(property.typeName.name, types),
types,
);
} else if (property.type === 'TSExpressionWithTypeArguments') {
return flattenProperties(
getProperties(property.expression.name, types),
types,
);
}
})
.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 = {
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,33 @@ 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 === 'TSInterfaceDeclaration' &&
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
@@ -186,6 +213,7 @@ function buildComponentSchema(ast): ComponentSchemaBuilderConfig {
const {
componentName,
propsTypeName,
stateTypeName,
commandTypeName,
commandOptionsExpression,
optionsExpression,
@@ -193,7 +221,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(
@@ -210,7 +238,7 @@ function buildComponentSchema(ast): ComponentSchemaBuilderConfig {
const events = getEvents(propProperties, types);
const commands = getCommands(commandProperties, types);
return {
const toRet = {
filename: componentName,
componentName,
options,
@@ -219,6 +247,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,601 +9,27 @@
*/
'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 alias = types[propsTypeName];
if (!alias) {
throw new Error(
`Failed to find definition for "${propsTypeName}", 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 "${propsTypeName}", please check that you have a valid codegen typescript file`,
);
}
}
function getTypeAnnotationForObjectAsArrayElement(
objectType: $FlowFixMe,
types: TypeDeclarationMap,
) {
return {
type: 'ObjectTypeAnnotation',
properties: flattenProperties(
objectType.typeParameters.params[0].members ||
objectType.typeParameters.params,
types,
)
.map(prop => buildPropSchema(prop, types))
.filter(Boolean),
};
}
function getTypeAnnotationForArrayOfArrayOfObject(
typeAnnotation: $FlowFixMe,
types: TypeDeclarationMap,
) {
// We need to go yet another level deeper to resolve
// types that may be defined in a type alias
const nestedObjectType = getValueFromTypes(typeAnnotation, types);
return {
type: 'ArrayTypeAnnotation',
elementType: getTypeAnnotationForObjectAsArrayElement(
nestedObjectType,
types,
),
};
}
function getTypeAnnotationForArray(
name: string,
typeAnnotation: $FlowFixMe,
defaultValue: $FlowFixMe | null,
types: TypeDeclarationMap,
) {
if (typeAnnotation.type === 'TSParenthesizedType') {
return getTypeAnnotationForArray(
name,
typeAnnotation.typeAnnotation,
defaultValue,
types,
);
}
const extractedTypeAnnotation = getValueFromTypes(typeAnnotation, types);
if (
extractedTypeAnnotation.type === 'TSUnionType' &&
extractedTypeAnnotation.types.some(
t => t.type === 'TSNullKeyword' || t.type === 'TSUndefinedKeyword',
)
) {
throw new Error(
'Nested optionals such as "ReadonlyArray<boolean | null | undefined>" are not supported, please declare optionals at the top level of value definitions as in "ReadonlyArray<boolean> | null | undefined"',
);
}
if (
extractedTypeAnnotation.type === 'TSTypeReference' &&
extractedTypeAnnotation.typeName.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>"',
);
}
// Covers: T[]
if (typeAnnotation.type === 'TSArrayType') {
return getTypeAnnotationForArrayOfArrayOfObject(
typeAnnotation.elementType,
types,
);
}
if (extractedTypeAnnotation.type === 'TSTypeReference') {
// Resolve the type alias if it's not defined inline
const objectType = getValueFromTypes(extractedTypeAnnotation, types);
if (objectType.typeName.name === 'Readonly') {
return getTypeAnnotationForObjectAsArrayElement(objectType, types);
}
// Covers: ReadonlyArray<T>
if (objectType.typeName.name === 'ReadonlyArray') {
return getTypeAnnotationForArrayOfArrayOfObject(
objectType.typeParameters.params[0],
types,
);
}
}
const type =
extractedTypeAnnotation.elementType === 'TSTypeReference'
? extractedTypeAnnotation.elementType.typeName.name
: extractedTypeAnnotation.elementType?.type ||
extractedTypeAnnotation.typeName?.name ||
extractedTypeAnnotation.type;
switch (type) {
case 'TSNumberKeyword':
return {
type: 'FloatTypeAnnotation',
};
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 'TSBooleanKeyword':
return {
type: 'BooleanTypeAnnotation',
};
case 'TSStringKeyword':
return {
type: 'StringTypeAnnotation',
};
case 'TSUnionType':
typeAnnotation.types.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 === null) {
throw new Error(`A default enum value is required for "${name}"`);
}
const unionType = typeAnnotation.types[0].type;
if (
unionType === 'TSLiteralType' &&
typeAnnotation.types[0].literal?.type === 'StringLiteral'
) {
return {
type: 'StringEnumTypeAnnotation',
default: (defaultValue: string),
options: typeAnnotation.types.map(option => option.literal.value),
};
} else if (
unionType === 'TSLiteralType' &&
typeAnnotation.types[0].literal?.type === 'NumericLiteral'
) {
throw new Error(
`Arrays of int enums are not supported (see: "${name}")`,
);
} else {
throw new Error(
`Unsupported union type for "${name}", received "${
unionType === 'TSLiteralType'
? typeAnnotation.types[0].literal?.type
: 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);
// Covers: (T)
if (typeAnnotation.type === 'TSParenthesizedType') {
return getTypeAnnotation(
name,
typeAnnotation.typeAnnotation,
defaultValue,
withNullDefault,
types,
);
}
// 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,
),
};
}
// Covers: ReadonlyArray<T>
if (
typeAnnotation.type === 'TSTypeReference' &&
typeAnnotation.typeName.name === 'ReadonlyArray'
) {
return {
type: 'ArrayTypeAnnotation',
elementType: getTypeAnnotationForArray(
name,
typeAnnotation.typeParameters.params[0],
defaultValue,
types,
),
};
}
// Covers: Readonly<T[]>
if (
typeAnnotation.type === 'TSTypeReference' &&
typeAnnotation.typeName?.name === 'Readonly' &&
typeAnnotation.typeParameters.type === 'TSTypeParameterInstantiation' &&
typeAnnotation.typeParameters.params[0].type === 'TSArrayType'
) {
return {
type: 'ArrayTypeAnnotation',
elementType: getTypeAnnotationForArray(
name,
typeAnnotation.typeParameters.params[0],
defaultValue,
types,
),
};
}
if (
(typeAnnotation.type === 'TSTypeReference' ||
typeAnnotation.type === 'TSTypeLiteral') &&
typeAnnotation.typeName?.name === 'Readonly'
) {
const rawProperties =
typeAnnotation.typeParameters.params[0].members ||
(typeAnnotation.typeParameters.params[0].types &&
typeAnnotation.typeParameters.params[0].types[0].members) ||
typeAnnotation.typeParameters.params;
const flattenedProperties = flattenProperties(rawProperties, types);
const properties = flattenedProperties
.map(prop => buildPropSchema(prop, types))
.filter(Boolean);
return {
type: 'ObjectTypeAnnotation',
properties,
};
}
const type =
typeAnnotation.type === 'TSTypeReference' ||
typeAnnotation.type === 'TSTypeAliasDeclaration'
? typeAnnotation.typeName.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 'TSBooleanKeyword':
return {
type: 'BooleanTypeAnnotation',
default: withNullDefault
? (defaultValue: boolean | null)
: ((defaultValue == null ? false : defaultValue): boolean),
};
case 'TSStringKeyword':
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 'TSUnionType':
typeAnnotation.types.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 === null) {
throw new Error(`A default enum value is required for "${name}"`);
}
const unionType = typeAnnotation.types[0].type;
if (
unionType === 'TSLiteralType' &&
typeAnnotation.types[0].literal?.type === 'StringLiteral'
) {
return {
type: 'StringEnumTypeAnnotation',
default: (defaultValue: string),
options: typeAnnotation.types.map(option => option.literal.value),
};
} else if (
unionType === 'TSLiteralType' &&
typeAnnotation.types[0].literal?.type === 'NumericLiteral'
) {
return {
type: 'Int32EnumTypeAnnotation',
default: (defaultValue: number),
options: typeAnnotation.types.map(option => option.literal.value),
};
} else {
throw new Error(
`Unsupported union type for "${name}", received "${
unionType === 'TSLiteralType'
? typeAnnotation.types[0].literal?.type
: unionType
}"`,
);
}
case 'TSNumberKeyword':
throw new Error(
`Cannot use "${type}" type annotation for "${name}": must use a specific numeric type like Int32, Double, or Float`,
);
default:
(type: empty);
throw new Error(`Unknown prop type for "${name}": "${type}"`);
}
}
function findProp(
name: string,
typeAnnotation: $FlowFixMe,
optionalType: boolean,
) {
switch (typeAnnotation.type) {
// Check for (T)
case 'TSParenthesizedType':
return findProp(name, typeAnnotation.typeAnnotation, optionalType);
// Check for optional type in union e.g. T | null | undefined
case 'TSUnionType':
return findProp(
name,
typeAnnotation.types.filter(
t => t.type !== 'TSNullKeyword' && t.type !== 'TSUndefinedKeyword',
)[0],
optionalType ||
typeAnnotation.types.some(
t => t.type === 'TSNullKeyword' || t.type === 'TSUndefinedKeyword',
),
);
case 'TSTypeReference':
// Check against optional type inside `WithDefault`
if (typeAnnotation.typeName.name === 'WithDefault' && optionalType) {
throw new Error(
'WithDefault<> is optional and does not need to be marked as optional. Please remove the union of undefined and/or null',
);
}
// Remove unwanted types
if (
typeAnnotation.typeName.name === 'DirectEventHandler' ||
typeAnnotation.typeName.name === 'BubblingEventHandler'
) {
return null;
}
if (
name === 'style' &&
typeAnnotation.type === 'GenericTypeAnnotation' &&
typeAnnotation.typeName.name === 'ViewStyleProp'
) {
return null;
}
return {typeAnnotation, optionalType};
default:
return {typeAnnotation, optionalType};
}
}
// $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.typeAnnotation.typeAnnotation,
types,
);
const foundProp = findProp(name, value, false);
if (!foundProp) {
const info = getSchemaInfo(property, types);
if (info == null) {
return null;
}
let {typeAnnotation, optionalType} = foundProp;
let optional = property.optional || optionalType;
// example: Readonly<{prop: string} | null | undefined>;
if (
value.type === 'TSTypeReference' &&
typeAnnotation.typeParameters?.params[0].type === 'TSUnionType' &&
typeAnnotation.typeParameters?.params[0].types.some(
element =>
element.type === 'TSNullKeyword' ||
element.type === 'TSUndefinedKeyword',
)
) {
optional = true;
}
if (
!property.optional &&
value.type === 'TSTypeReference' &&
typeAnnotation.typeName.name === 'WithDefault'
) {
throw new Error(
`key ${name} must be optional if used with WithDefault<> annotation`,
);
}
let type = typeAnnotation.type;
let defaultValue = null;
let withNullDefault = false;
if (
type === 'TSTypeReference' &&
typeAnnotation.typeName.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}"?`,
);
}
let defaultValueType = typeAnnotation.typeParameters.params[1].type;
defaultValue = typeAnnotation.typeParameters.params[1].value;
if (defaultValueType === 'TSLiteralType') {
defaultValueType = typeAnnotation.typeParameters.params[1].literal.type;
defaultValue = typeAnnotation.typeParameters.params[1].literal.value;
}
if (defaultValueType === 'TSNullKeyword') {
defaultValue = null;
withNullDefault = true;
}
typeAnnotation = typeAnnotation.typeParameters.params[0];
type =
typeAnnotation.type === 'TSTypeReference'
? typeAnnotation.typeName.name
: typeAnnotation.type;
}
const {name, optional, typeAnnotation, defaultValue, withNullDefault} = info;
return {
name,
optional,
@@ -613,72 +39,20 @@ function buildPropSchema(
defaultValue,
withNullDefault,
types,
buildPropSchema,
),
};
}
// $FlowFixMe[unclear-type] TODO(T108222691): Use flow-types for @babel/parser
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 === 'TSPropertySignature') {
return property;
} else if (property.type === 'TSTypeReference') {
return flattenProperties(
getPropProperties(property.typeName.name, types),
types,
);
} else if (property.type === 'TSExpressionWithTypeArguments') {
return flattenProperties(
getPropProperties(property.expression.name, types),
types,
);
}
})
.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);
}
function getProps(
typeDefinition: $ReadOnlyArray<PropAST>,
types: TypeDeclarationMap,
): $ReadOnlyArray<NamedShape<PropTypeAnnotation>> {
return flattenProperties(typeDefinition, types)
.map(property => {
return buildPropSchema(property, types);
})
.map(property => buildPropSchema(property, types))
.filter(Boolean);
}
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,58 @@
/**
* Copyright (c) Meta Platforms, Inc. 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,
};