mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
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:
committed by
Facebook GitHub Bot
parent
114d5a8a17
commit
c532fcff90
@@ -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,
|
||||
};
|
||||
Reference in New Issue
Block a user