Copy Flow parser components logic to prepare TypeScript parser

Summary:
These files are directly copied from the flow parser to aid in reviewing the next Diff, D33080623

Changelog:
[Internal][Add] - Copy Flow parser component logic to aid in Diff review

Reviewed By: RSNara

Differential Revision: D33130222

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