diff --git a/Libraries/Utilities/codegenNativeCommands.js b/Libraries/Utilities/codegenNativeCommands.js index a6dc594f642..b0daa447d7f 100644 --- a/Libraries/Utilities/codegenNativeCommands.js +++ b/Libraries/Utilities/codegenNativeCommands.js @@ -10,7 +10,11 @@ 'use strict'; -function codegenNativeCommands(): T { +type Options = $ReadOnly<{| + supportedCommands: $ReadOnlyArray, +|}>; + +function codegenNativeCommands(options: Options<$Keys>): T { return (({}: any): T); } diff --git a/packages/babel-plugin-inline-view-configs/__test_fixtures__/fixtures.js b/packages/babel-plugin-inline-view-configs/__test_fixtures__/fixtures.js index c40d4ed27f7..9f0925af87f 100644 --- a/packages/babel-plugin-inline-view-configs/__test_fixtures__/fixtures.js +++ b/packages/babel-plugin-inline-view-configs/__test_fixtures__/fixtures.js @@ -46,7 +46,9 @@ type ModuleProps = $ReadOnly<{| onBubblingEventDefinedInlineNull: BubblingEventHandler, |}>; -export const Commands = codegenNativeCommands(); +export const Commands = codegenNativeCommands({ + supportedCommands: ['hotspotUpdate', 'scrollTo'], +}); export default codegenNativeComponent('Module', { interfaceOnly: true, diff --git a/packages/babel-plugin-inline-view-configs/package.json b/packages/babel-plugin-inline-view-configs/package.json index d61d30de9b3..ad1dc6e0d0c 100644 --- a/packages/babel-plugin-inline-view-configs/package.json +++ b/packages/babel-plugin-inline-view-configs/package.json @@ -1,5 +1,5 @@ { - "version": "0.0.3", + "version": "0.0.4", "name": "babel-plugin-inline-view-configs", "description": "Babel plugin to inline view configs for React Native", "repository": { diff --git a/packages/react-native-codegen/src/parsers/flow/components/__test_fixtures__/failures.js b/packages/react-native-codegen/src/parsers/flow/components/__test_fixtures__/failures.js index d9a2fafd1b8..de68dd37b1b 100644 --- a/packages/react-native-codegen/src/parsers/flow/components/__test_fixtures__/failures.js +++ b/packages/react-native-codegen/src/parsers/flow/components/__test_fixtures__/failures.js @@ -41,7 +41,9 @@ export type ModuleProps = $ReadOnly<{| export const Commands = codegenNativeCommands<{ +hotspotUpdate: (ref: React.Ref<'RCTView'>, x: Int32, y: Int32) => void; -}>(); +}>({ + supportedCommands: ['hotspotUpdate'] +}); export default codegenNativeComponent('Module'); `; @@ -79,8 +81,12 @@ export type ModuleProps = $ReadOnly<{| // No props or events |}>; -export const Commands = codegenNativeCommands(); -export const Commands2 = codegenNativeCommands(); +export const Commands = codegenNativeCommands({ + supportedCommands: ['hotspotUpdate'] +}); +export const Commands2 = codegenNativeCommands({ + supportedCommands: ['hotspotUpdate'] +}); export default codegenNativeComponent('Module'); `; @@ -118,10 +124,93 @@ export type ModuleProps = $ReadOnly<{| // No props or events |}>; +export const Commands = codegenNativeCommands({ + supportedCommands: ['hotspotUpdate'] +}); + +export default codegenNativeComponent('Module'); +`; + +const COMMANDS_DEFINED_WITH_MISMATCHED_METHOD_NAMES = ` +/** + * 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 + */ + +'use strict'; + +const codegenNativeComponent = require('codegenNativeComponent'); +const codegenNativeCommands = require('codegenNativeCommands'); + +import type { + Int32, + BubblingEventHandler, + DirectEventHandler, +} from 'CodegenTypes'; + +import type {ViewProps} from 'ViewPropTypes'; + +interface NativeCommands { + +hotspotUpdate: (viewRef: React.Ref<'RCTView'>, x: Int32, y: Int32) => void; + +scrollTo: (viewRef: React.Ref<'RCTView'>, y: Int32, animated: boolean) => void; +} + +export type ModuleProps = $ReadOnly<{| + ...ViewProps, + // No props or events +|}>; + +export const Commands = codegenNativeCommands({ + supportedCommands: ['scrollTo'] +}); + +export default codegenNativeComponent('Module'); +`; + +const COMMANDS_DEFINED_WITHOUT_METHOD_NAMES = ` +/** + * 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 + */ + +'use strict'; + +const codegenNativeComponent = require('codegenNativeComponent'); +const codegenNativeCommands = require('codegenNativeCommands'); + +import type { + Int32, + BubblingEventHandler, + DirectEventHandler, +} from 'CodegenTypes'; + +import type {ViewProps} from 'ViewPropTypes'; + +interface NativeCommands { + +hotspotUpdate: (viewRef: React.Ref<'RCTView'>, x: Int32, y: Int32) => void; + +scrollTo: (viewRef: React.Ref<'RCTView'>, y: Int32, animated: boolean) => void; +} + +export type ModuleProps = $ReadOnly<{| + ...ViewProps, + // No props or events +|}>; + export const Commands = codegenNativeCommands(); export default codegenNativeComponent('Module'); `; + const NULLABLE_WITH_DEFAULT = ` /** * Copyright (c) Facebook, Inc. and its affiliates. @@ -186,6 +275,8 @@ export default codegenNativeComponent('Module'); module.exports = { COMMANDS_DEFINED_INLINE, COMMANDS_DEFINED_MULTIPLE_TIMES, + COMMANDS_DEFINED_WITH_MISMATCHED_METHOD_NAMES, + COMMANDS_DEFINED_WITHOUT_METHOD_NAMES, COMMANDS_DEFINED_WITHOUT_REF, NULLABLE_WITH_DEFAULT, NON_OPTIONAL_KEY_WITH_DEFAULT_VALUE, diff --git a/packages/react-native-codegen/src/parsers/flow/components/__test_fixtures__/fixtures.js b/packages/react-native-codegen/src/parsers/flow/components/__test_fixtures__/fixtures.js index a70b7846919..babaa9dc8f8 100644 --- a/packages/react-native-codegen/src/parsers/flow/components/__test_fixtures__/fixtures.js +++ b/packages/react-native-codegen/src/parsers/flow/components/__test_fixtures__/fixtures.js @@ -559,7 +559,9 @@ export type ModuleProps = $ReadOnly<{| // No props or events |}>; -export const Commands = codegenNativeCommands(); +export const Commands = codegenNativeCommands({ + supportedCommands: ['hotspotUpdate', 'scrollTo'] +}); export default codegenNativeComponent('Module'); `; @@ -603,7 +605,9 @@ export type ModuleProps = $ReadOnly<{| // No props or events |}>; -export const Commands = codegenNativeCommands(); +export const Commands = codegenNativeCommands({ + supportedCommands: ['scrollTo'] +}); export default codegenNativeComponent('Module'); `; @@ -656,7 +660,9 @@ export type ModuleProps = $ReadOnly<{| onDirectEventDefinedInlineWithPaperName: DirectEventHandler, |}>; -export const Commands = codegenNativeCommands(); +export const Commands = codegenNativeCommands({ + supportedCommands: ['scrollTo'] +}); export default codegenNativeComponent('Module'); `; diff --git a/packages/react-native-codegen/src/parsers/flow/components/__tests__/__snapshots__/component-parser-test.js.snap b/packages/react-native-codegen/src/parsers/flow/components/__tests__/__snapshots__/component-parser-test.js.snap index dc4046fc89f..8c57f4725c8 100644 --- a/packages/react-native-codegen/src/parsers/flow/components/__tests__/__snapshots__/component-parser-test.js.snap +++ b/packages/react-native-codegen/src/parsers/flow/components/__tests__/__snapshots__/component-parser-test.js.snap @@ -4,6 +4,10 @@ exports[`RN Codegen Flow Parser Fails with error message COMMANDS_DEFINED_INLINE exports[`RN Codegen Flow Parser Fails with error message COMMANDS_DEFINED_MULTIPLE_TIMES 1`] = `"codegenNativeCommands may only be called once in a file"`; +exports[`RN Codegen Flow Parser Fails with error message COMMANDS_DEFINED_WITH_MISMATCHED_METHOD_NAMES 1`] = `"codegenNativeCommands expected the same supportedCommands specified in the NativeCommands interface: hotspotUpdate, scrollTo"`; + +exports[`RN Codegen Flow Parser Fails with error message COMMANDS_DEFINED_WITHOUT_METHOD_NAMES 1`] = `"codegenNativeCommands must be passed options including the supported commands"`; + exports[`RN Codegen Flow Parser Fails with error message COMMANDS_DEFINED_WITHOUT_REF 1`] = `"The first argument of method hotspotUpdate must be of type React.Ref<>"`; exports[`RN Codegen Flow Parser Fails with error message NON_OPTIONAL_KEY_WITH_DEFAULT_VALUE 1`] = `"key required_key_with_default must be optional if used with WithDefault<> annotation"`; diff --git a/packages/react-native-codegen/src/parsers/flow/components/index.js b/packages/react-native-codegen/src/parsers/flow/components/index.js index c683850ede1..52570c3da92 100644 --- a/packages/react-native-codegen/src/parsers/flow/components/index.js +++ b/packages/react-native-codegen/src/parsers/flow/components/index.js @@ -14,7 +14,7 @@ import type {ComponentSchemaBuilderConfig} from './schema.js'; const {getCommands} = require('./commands'); const {getEvents} = require('./events'); const {getProps} = require('./props'); -const {getOptions} = require('./options'); +const {getCommandOptions, getOptions} = require('./options'); const {getExtendsProps} = require('./extends'); function findComponentConfig(ast) { @@ -58,11 +58,12 @@ function findComponentConfig(ast) { const commandsTypeNames = namedExports .map(statement => { + let callExpression; let calleeName; try { - calleeName = statement.declaration.declarations[0].init.callee.name; + callExpression = statement.declaration.declarations[0].init; + calleeName = callExpression.callee.name; } catch (e) { - // Not a function call return; } @@ -70,8 +71,14 @@ function findComponentConfig(ast) { return; } - const typeArgumentParam = - statement.declaration.declarations[0].init.typeArguments.params[0]; + // 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( @@ -79,7 +86,10 @@ function findComponentConfig(ast) { ); } - return typeArgumentParam.id.name; + return { + commandTypeName: typeArgumentParam.id.name, + commandOptionsExpression: callExpression.arguments[0], + }; }) .filter(Boolean); @@ -89,7 +99,14 @@ function findComponentConfig(ast) { return { ...foundConfig, - commandTypeName: commandsTypeNames[0], + commandTypeName: + commandsTypeNames[0] == null + ? null + : commandsTypeNames[0].commandTypeName, + commandOptionsExpression: + commandsTypeNames[0] == null + ? null + : commandsTypeNames[0].commandOptionsExpression, }; } @@ -104,7 +121,7 @@ function getPropProperties(propsTypeName, types) { } } -function getCommandProperties(commandTypeName, types) { +function getCommandProperties(commandTypeName, types, commandOptions) { if (commandTypeName == null) { return []; } @@ -119,13 +136,39 @@ function getCommandProperties(commandTypeName, types) { ); } + let properties; try { - return typeAlias.body.properties; + 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 there's no flowtype for AST @@ -134,11 +177,18 @@ function processComponent(ast, types): ComponentSchemaBuilderConfig { componentName, propsTypeName, commandTypeName, + commandOptionsExpression, optionsExpression, } = findComponentConfig(ast); const propProperties = getPropProperties(propsTypeName, types); - const commandProperties = getCommandProperties(commandTypeName, types); + const commandOptions = getCommandOptions(commandOptionsExpression); + + const commandProperties = getCommandProperties( + commandTypeName, + types, + commandOptions, + ); const extendsProps = getExtendsProps(propProperties); const options = getOptions(optionsExpression); diff --git a/packages/react-native-codegen/src/parsers/flow/components/options.js b/packages/react-native-codegen/src/parsers/flow/components/options.js index 23ada1a6be1..f455a5f060a 100644 --- a/packages/react-native-codegen/src/parsers/flow/components/options.js +++ b/packages/react-native-codegen/src/parsers/flow/components/options.js @@ -15,6 +15,38 @@ import type {OptionsShape} from '../../../CodegenSchema.js'; // $FlowFixMe there's no flowtype for ASTs type OptionsAST = Object; +export type CommandOptions = $ReadOnly<{| + supportedCommands: $ReadOnlyArray, +|}>; + +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; @@ -44,5 +76,6 @@ function getOptions(optionsExpression: OptionsAST): ?OptionsShape { } module.exports = { + getCommandOptions, getOptions, };