Update View Config generator to create command methods

Summary:
Flow types like this:
```
interface NativeCommands {
  +hotspotUpdate: (viewRef: React.Ref<'RCTView'>, x: Int32, y: Int32) => void;
}

export const Commands = codegenNativeCommands<NativeCommands>();
```

get turned into this:

```
export const Commands = {
  hotspotUpdate(viewRef: React.Ref<'RCTView'>, x: number, y: number) {
    UIManager.dispatchViewCommand(
      findNodeHandle(viewRef),
      UIManager.getViewManagerConfig('RCTView').Commands.hotspotUpdate,
      [x, y]
    );
  }
}
```

Reviewed By: rickhanlonii

Differential Revision: D15953126

fbshipit-source-id: edbb91056347d021dd0683391c903b76f3d1c33f
This commit is contained in:
Eli White
2019-06-24 18:54:41 -07:00
committed by Facebook Github Bot
parent 9ad60131ba
commit 2bd503285e
13 changed files with 454 additions and 8 deletions
@@ -0,0 +1,17 @@
/**
* 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';
function codegenNativeCommands<T>(): T {
return (({}: any): T);
}
export default codegenNativeCommands;
@@ -16,9 +16,13 @@ export default 'Not a view config'
`;
const FULL_NATIVE_COMPONENT = `
// @flow
const codegenNativeCommands = require('codegenNativeCommands');
const codegenNativeComponent = require('codegenNativeComponent');
import type {
Int32,
BubblingEvent,
DirectEvent,
WithDefault,
@@ -26,6 +30,11 @@ import type {
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;
}
type ModuleProps = $ReadOnly<{|
...ViewProps,
@@ -37,6 +46,8 @@ type ModuleProps = $ReadOnly<{|
onBubblingEventDefinedInlineNull: (event: BubblingEvent<null>) => void,
|}>;
export const Commands = codegenNativeCommands<NativeCommands>();
export default codegenNativeComponent<ModuleProps>('Module', {
interfaceOnly: true,
paperComponentName: 'RCTModule',
@@ -1,10 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Babel plugin inline view configs can inline config for FullNativeComponent.js 1`] = `
"const codegenNativeComponent = require('codegenNativeComponent');
"// @flow
const codegenNativeCommands = require('codegenNativeCommands');
import type { BubblingEvent, DirectEvent, WithDefault } from 'CodegenFlowtypes';
const codegenNativeComponent = require('codegenNativeComponent');
import type { Int32, BubblingEvent, DirectEvent, WithDefault } from 'CodegenFlowtypes';
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,
}
type ModuleProps = $ReadOnly<{| ...ViewProps,
// Props
boolean_default_true_optional_both?: ?WithDefault<boolean, true>,
@@ -12,9 +19,18 @@ type ModuleProps = $ReadOnly<{| ...ViewProps,
onDirectEventDefinedInlineNull: (event: DirectEvent<null>) => void,
onBubblingEventDefinedInlineNull: (event: BubblingEvent<null>) => void,
|}>;
export const Commands = codegenNativeCommands<NativeCommands>();
const registerGeneratedViewConfig = require('registerGeneratedViewConfig');
const {
UIManager
} = require(\\"react-native\\");
const {
findNodeHandle
} = require(\\"react-native\\");
const ModuleViewConfig = {
uiViewClassName: 'RCTModule',
bubblingEventTypes: {
@@ -39,7 +55,17 @@ const ModuleViewConfig = {
let nativeComponentName = 'RCTModule';
registerGeneratedViewConfig(nativeComponentName, ModuleViewConfig);
export const __INTERNAL_VIEW_CONFIG = ModuleViewConfig;
export default nativeComponentName;"
export default nativeComponentName;
export const Commands = {
hotspotUpdate(ref, x, y) {
UIManager.dispatchViewCommand(findNodeHandle(ref), UIManager.getViewManagerConfig(\\"RCTModule\\").Commands.hotspotUpdate, [x, y]);
},
scrollTo(ref, y, animated) {
UIManager.dispatchViewCommand(findNodeHandle(ref), UIManager.getViewManagerConfig(\\"RCTModule\\").Commands.scrollTo, [y, animated]);
}
};"
`;
exports[`Babel plugin inline view configs can inline config for NotANativeComponent.js 1`] = `
@@ -34,6 +34,10 @@ const template = `
::_COMPONENT_CONFIG_::
`;
// We use this to add to a set. Need to make sure we aren't importing
// this multiple times.
const UIMANAGER_IMPORT = 'const {UIManager} = require("react-native")';
function getReactDiffProcessValue(typeAnnotation) {
switch (typeAnnotation.type) {
case 'BooleanTypeAnnotation':
@@ -239,6 +243,69 @@ function buildViewConfig(
return j.objectExpression(properties);
}
function buildCommands(
schema: SchemaType,
componentName: string,
component,
imports,
) {
const commands = component.commands;
if (commands.length === 0) {
return null;
}
imports.add(UIMANAGER_IMPORT);
imports.add('const {findNodeHandle} = require("react-native")');
const properties = commands.map(command => {
const commandName = command.name;
const params = command.typeAnnotation.params;
const componentNameLiteral = j.literal(componentName);
const commandNameIdentifier = j.identifier(commandName);
const arrayParams = j.arrayExpression(
params.map(param => {
return j.identifier(param.name);
}),
);
const expression = j.template.expression`
UIManager.dispatchViewCommand(
findNodeHandle(ref),
UIManager.getViewManagerConfig(${componentNameLiteral}).Commands.${commandNameIdentifier},
${arrayParams}
)
`;
const functionParams = params.map(param => {
return j.identifier(param.name);
});
const property = j.property(
'init',
commandNameIdentifier,
j.functionExpression(
null,
[j.identifier('ref'), ...functionParams],
j.blockStatement([j.expressionStatement(expression)]),
),
);
property.method = true;
return property;
});
return j.exportNamedDeclaration(
j.variableDeclaration('const', [
j.variableDeclarator(
j.identifier('Commands'),
j.objectExpression(properties),
),
]),
);
}
module.exports = {
generate(libraryName: string, schema: SchemaType): FilesOutput {
try {
@@ -262,7 +329,7 @@ module.exports = {
: componentName;
if (component.paperComponentNameDeprecated) {
imports.add('const {UIManager} = require("react-native")');
imports.add(UIMANAGER_IMPORT);
}
const deprecatedCheckBlock = component.paperComponentNameDeprecated
@@ -282,8 +349,9 @@ module.exports = {
)
.replace(/::_DEPRECATION_CHECK_::/, deprecatedCheckBlock);
const replacedSource: string = j
.withParser('flow')(replacedTemplate)
const replacedSourceRoot = j.withParser('flow')(replacedTemplate);
replacedSourceRoot
.find(j.Identifier, {
name: 'VIEW_CONFIG',
})
@@ -294,8 +362,24 @@ module.exports = {
component,
imports,
),
)
.toSource({quote: 'single', trailingComma: true});
);
const commands = buildCommands(
schema,
paperComponentName,
component,
imports,
);
if (commands) {
replacedSourceRoot
.find(j.ExportDefaultDeclaration)
.insertAfter(j(commands).toSource());
}
const replacedSource: string = replacedSourceRoot.toSource({
quote: 'single',
trailingComma: true,
});
return replacedSource;
})
@@ -846,6 +846,69 @@ const TWO_COMPONENTS_DIFFERENT_FILES: SchemaType = {
},
};
const COMMANDS: SchemaType = {
modules: {
Switch: {
components: {
CommandNativeComponent: {
extendsProps: [
{
type: 'ReactNativeBuiltInType',
knownTypeName: 'ReactNativeCoreViewProps',
},
],
events: [],
props: [],
commands: [
{
name: 'hotspotUpdate',
optional: false,
typeAnnotation: {
type: 'FunctionTypeAnnotation',
params: [
{
name: 'x',
typeAnnotation: {
type: 'Int32TypeAnnotation',
},
},
{
name: 'y',
typeAnnotation: {
type: 'Int32TypeAnnotation',
},
},
],
},
},
{
name: 'scrollTo',
optional: false,
typeAnnotation: {
type: 'FunctionTypeAnnotation',
params: [
{
name: 'y',
typeAnnotation: {
type: 'Int32TypeAnnotation',
},
},
{
name: 'animated',
typeAnnotation: {
type: 'BooleanTypeAnnotation',
},
},
],
},
},
],
},
},
},
},
};
module.exports = {
NO_PROPS_NO_EVENTS,
INTERFACE_ONLY,
@@ -863,4 +926,5 @@ module.exports = {
EVENT_NESTED_OBJECT_PROPS,
TWO_COMPONENTS_SAME_FILE,
TWO_COMPONENTS_DIFFERENT_FILES,
COMMANDS,
};
@@ -63,6 +63,29 @@ namespace react {
} // namespace react
} // namespace facebook
",
}
`;
exports[`GenerateEventEmitterCpp can generate fixture COMMANDS 1`] = `
Map {
"EventEmitters.cpp" => "
/**
* 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.
*/
#include <react/components/COMMANDS/EventEmitters.h>
namespace facebook {
namespace react {
} // namespace react
} // namespace facebook
",
@@ -66,6 +66,30 @@ namespace react {
} // namespace react
} // namespace facebook
",
}
`;
exports[`GenerateEventEmitterH can generate fixture COMMANDS 1`] = `
Map {
"EventEmitters.h" => "
/**
* 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.
*/
#pragma once
#include <react/components/view/ViewEventEmitter.h>
namespace facebook {
namespace react {
} // namespace react
} // namespace facebook
",
@@ -95,6 +95,35 @@ ColorPropNativeComponentProps::ColorPropNativeComponentProps(
}
`;
exports[`GeneratePropsCpp can generate fixture COMMANDS 1`] = `
Map {
"Props.cpp" => "
/**
* 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.
*/
#include <react/components/COMMANDS/Props.h>
#include <react/core/propsConversions.h>
namespace facebook {
namespace react {
CommandNativeComponentProps::CommandNativeComponentProps(
const CommandNativeComponentProps &sourceProps,
const RawProps &rawProps): ViewProps(sourceProps, rawProps)
{}
} // namespace react
} // namespace facebook
",
}
`;
exports[`GeneratePropsCpp can generate fixture ENUM_PROP 1`] = `
Map {
"Props.cpp" => "
@@ -157,6 +157,38 @@ class ColorPropNativeComponentProps final : public ViewProps {
}
`;
exports[`GeneratePropsH can generate fixture COMMANDS 1`] = `
Map {
"Props.h" => "
/**
* 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.
*/
#pragma once
#include <react/components/view/ViewProps.h>
namespace facebook {
namespace react {
class CommandNativeComponentProps final : public ViewProps {
public:
CommandNativeComponentProps() = default;
CommandNativeComponentProps(const CommandNativeComponentProps &sourceProps, const RawProps &rawProps);
#pragma mark - Props
};
} // namespace react
} // namespace facebook
",
}
`;
exports[`GeneratePropsH can generate fixture ENUM_PROP 1`] = `
Map {
"Props.h" => "
@@ -69,6 +69,29 @@ extern const char ColorPropNativeComponentComponentName[] = \\"ColorPropNativeCo
}
`;
exports[`GenerateShadowNodeCpp can generate fixture COMMANDS 1`] = `
Map {
"ShadowNodes.cpp" => "
/**
* 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.
*/
#include <react/components/COMMANDS/ShadowNodes.h>
namespace facebook {
namespace react {
extern const char CommandNativeComponentComponentName[] = \\"CommandNativeComponent\\";
} // namespace react
} // namespace facebook
",
}
`;
exports[`GenerateShadowNodeCpp can generate fixture ENUM_PROP 1`] = `
Map {
"ShadowNodes.cpp" => "
@@ -99,6 +99,39 @@ using ColorPropNativeComponentShadowNode = ConcreteViewShadowNode<
}
`;
exports[`GenerateShadowNodeH can generate fixture COMMANDS 1`] = `
Map {
"ShadowNodes.h" => "
/**
* 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.
*/
#pragma once
#include <react/components/COMMANDS/Props.h>
#include <react/components/view/ConcreteViewShadowNode.h>
namespace facebook {
namespace react {
extern const char CommandNativeComponentComponentName[];
/*
* \`ShadowNode\` for <CommandNativeComponent> component.
*/
using CommandNativeComponentShadowNode = ConcreteViewShadowNode<
CommandNativeComponentComponentName,
CommandNativeComponentProps>;
} // namespace react
} // namespace facebook
",
}
`;
exports[`GenerateShadowNodeH can generate fixture ENUM_PROP 1`] = `
Map {
"ShadowNodes.h" => "
@@ -103,6 +103,34 @@ TEST(ColorPropNativeComponentProps_tintColor, etc) {
}
`;
exports[`GenerateTests can generate fixture COMMANDS 1`] = `
Map {
"Tests.cpp" => "/**
* 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.
*/
#include <gtest/gtest.h>
#include <react/components/COMMANDS/Props.h>
#include <react/core/RawProps.h>
#include <react/core/RawPropsParser.h>
#include <react/core/propsConversions.h>
using namespace facebook::react;
TEST(CommandNativeComponentProps_DoesNotDie, etc) {
auto propParser = RawPropsParser();
propParser.prepare<CommandNativeComponentProps>();
auto const &sourceProps = CommandNativeComponentProps();
auto const &rawProps = RawProps(folly::dynamic::object(\\"xx_invalid_xx\\", \\"xx_invalid_xx\\"));
rawProps.parse(propParser);
CommandNativeComponentProps(sourceProps, rawProps);
}",
}
`;
exports[`GenerateTests can generate fixture ENUM_PROP 1`] = `
Map {
"Tests.cpp" => "/**
@@ -112,6 +112,58 @@ export default nativeComponentName;
}
`;
exports[`GenerateViewConfigJs can generate fixture COMMANDS 1`] = `
Map {
"COMMANDSNativeViewConfig.js" => "
/**
* 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
*/
'use strict';
const registerGeneratedViewConfig = require('registerGeneratedViewConfig');
const {UIManager} = require(\\"react-native\\")
const {findNodeHandle} = require(\\"react-native\\")
const CommandNativeComponentViewConfig = {
uiViewClassName: 'CommandNativeComponent',
validAttributes: {},
};
let nativeComponentName = 'CommandNativeComponent';
registerGeneratedViewConfig(nativeComponentName, CommandNativeComponentViewConfig);
export const __INTERNAL_VIEW_CONFIG = CommandNativeComponentViewConfig;
export default nativeComponentName;
export const Commands = {
hotspotUpdate(ref, x, y) {
UIManager.dispatchViewCommand(
findNodeHandle(ref),
UIManager.getViewManagerConfig(\\"CommandNativeComponent\\").Commands.hotspotUpdate,
[x, y]
);
},
scrollTo(ref, y, animated) {
UIManager.dispatchViewCommand(
findNodeHandle(ref),
UIManager.getViewManagerConfig(\\"CommandNativeComponent\\").Commands.scrollTo,
[y, animated]
);
}
};
",
}
`;
exports[`GenerateViewConfigJs can generate fixture ENUM_PROP 1`] = `
Map {
"ENUM_PROPNativeViewConfig.js" => "