diff --git a/packages/react-native-codegen/src/generators/components/GeneratePropsJavaInterface.js b/packages/react-native-codegen/src/generators/components/GeneratePropsJavaInterface.js new file mode 100644 index 00000000000..235230d13ff --- /dev/null +++ b/packages/react-native-codegen/src/generators/components/GeneratePropsJavaInterface.js @@ -0,0 +1,143 @@ +/** + * 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 + * @format + */ + +'use strict'; + +import type { + ComponentShape, + PropTypeShape, + SchemaType, +} from '../../CodegenSchema'; +const {getImports, toSafeJavaString} = require('./JavaHelpers'); + +// File path -> contents +type FilesOutput = Map; + +const template = ` +package com.facebook.react.viewmanagers; + +::_IMPORTS_:: + +public interface ::_CLASSNAME_:: { + ::_PROP_SETTERS_:: +} +`; + +function getJavaValueForProp( + prop: PropTypeShape, + componentName: string, +): string { + const typeAnnotation = prop.typeAnnotation; + + switch (typeAnnotation.type) { + case 'BooleanTypeAnnotation': + return 'boolean value'; + case 'StringTypeAnnotation': + return 'String value'; + case 'Int32TypeAnnotation': + return 'int value'; + case 'FloatTypeAnnotation': + return 'Float value'; + case 'NativePrimitiveTypeAnnotation': + switch (typeAnnotation.name) { + case 'ColorPrimitive': + return 'Integer value'; + case 'ImageSourcePrimitive': + return 'ReadableMap value'; + case 'PointPrimitive': + return 'ReadableMap value'; + default: + (typeAnnotation.name: empty); + throw new Error('Receieved unknown NativePrimitiveTypeAnnotation'); + } + case 'ArrayTypeAnnotation': { + return 'ReadableArray value'; + } + case 'StringEnumTypeAnnotation': + return 'String value'; + default: + (typeAnnotation: empty); + throw new Error('Receieved invalid typeAnnotation'); + } +} + +function generatePropsString(component: ComponentShape, componentName: string) { + if (component.props.length === 0) { + return ' // No props'; + } + + return component.props + .map(prop => { + return `void set${toSafeJavaString( + prop.name, + )}(T view, ${getJavaValueForProp(prop, componentName)});`; + }) + .join(',\n' + ' '); +} + +function getClassExtendString(component): string { + const extendString = component.extendsProps + .map(extendProps => { + switch (extendProps.type) { + case 'ReactNativeBuiltInType': + switch (extendProps.knownTypeName) { + case 'ReactNativeCoreViewProps': + return 'View'; + default: + (extendProps.knownTypeName: empty); + throw new Error('Invalid knownTypeName'); + } + default: + (extendProps.type: empty); + throw new Error('Invalid extended type'); + } + }) + .join(''); + + return extendString; +} + +module.exports = { + generate(libraryName: string, schema: SchemaType): FilesOutput { + const files = new Map(); + Object.keys(schema.modules).forEach(moduleName => { + const components = schema.modules[moduleName].components; + // No components in this module + if (components == null) { + return; + } + + return Object.keys(components).forEach(componentName => { + const component = components[componentName]; + const className = `${componentName}Interface`; + const fileName = `${className}.java`; + + const imports = getImports(component); + const propsString = generatePropsString(component, componentName); + const extendString = getClassExtendString(component); + + const replacedTemplate = template + .replace( + /::_IMPORTS_::/g, + Array.from(imports) + .sort() + .join('\n'), + ) + .replace(/::_CLASSNAME_::/g, className) + .replace('::_EXTEND_CLASSES_::', extendString) + .replace('::_PROP_SETTERS_::', propsString); + + files.set(fileName, replacedTemplate); + }); + }); + + return files; + }, +}; diff --git a/packages/react-native-codegen/src/generators/components/JavaHelpers.js b/packages/react-native-codegen/src/generators/components/JavaHelpers.js new file mode 100644 index 00000000000..7f5ae1e94e4 --- /dev/null +++ b/packages/react-native-codegen/src/generators/components/JavaHelpers.js @@ -0,0 +1,77 @@ +/** + * 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 + * @format + */ + +'use strict'; + +import type {ComponentShape} from '../../CodegenSchema'; + +function upperCaseFirst(inString: string): string { + return inString[0].toUpperCase() + inString.slice(1); +} + +export function toSafeJavaString(input: string): string { + return input + .split('-') + .map(upperCaseFirst) + .join(''); +} + +export function getImports(component: ComponentShape): Set { + const imports: Set = new Set(); + + component.extendsProps.forEach(extendProps => { + switch (extendProps.type) { + case 'ReactNativeBuiltInType': + switch (extendProps.knownTypeName) { + case 'ReactNativeCoreViewProps': + imports.add('import android.view.View;'); + return; + default: + (extendProps.knownTypeName: empty); + throw new Error('Invalid knownTypeName'); + } + default: + (extendProps.type: empty); + throw new Error('Invalid extended type'); + } + }); + + function addImportsForNativeName(name) { + switch (name) { + case 'ColorPrimitive': + return; + case 'ImageSourcePrimitive': + imports.add('import com.facebook.react.bridge.ReadableMap;'); + return; + case 'PointPrimitive': + imports.add('import com.facebook.react.bridge.ReadableMap;'); + return; + default: + (name: empty); + throw new Error( + `Invalid NativePrimitiveTypeAnnotation name, got ${name}`, + ); + } + } + + component.props.forEach(prop => { + const typeAnnotation = prop.typeAnnotation; + + if (typeAnnotation.type === 'NativePrimitiveTypeAnnotation') { + addImportsForNativeName(typeAnnotation.name); + } + + if (typeAnnotation.type === 'ArrayTypeAnnotation') { + imports.add('import com.facebook.react.bridge.ReadableArray;'); + } + }); + + return imports; +} diff --git a/packages/react-native-codegen/src/generators/components/__tests__/GeneratePropsJavaInterface-test.js b/packages/react-native-codegen/src/generators/components/__tests__/GeneratePropsJavaInterface-test.js new file mode 100644 index 00000000000..cc1eadab41a --- /dev/null +++ b/packages/react-native-codegen/src/generators/components/__tests__/GeneratePropsJavaInterface-test.js @@ -0,0 +1,27 @@ +/** + * 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. + * + * @emails oncall+react_native + * @flow strict-local + * @format + */ + +'use strict'; + +const fixtures = require('../__test_fixtures__/fixtures.js'); +const generator = require('../GeneratePropsJavaInterface.js'); + +describe('GeneratePropsJavaInterface', () => { + Object.keys(fixtures) + .sort() + .forEach(fixtureName => { + const fixture = fixtures[fixtureName]; + + it(`can generate fixture ${fixtureName}`, () => { + expect(generator.generate(fixtureName, fixture)).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/react-native-codegen/src/generators/components/__tests__/__snapshots__/GeneratePropsJavaInterface-test.js.snap b/packages/react-native-codegen/src/generators/components/__tests__/__snapshots__/GeneratePropsJavaInterface-test.js.snap new file mode 100644 index 00000000000..a76af20d331 --- /dev/null +++ b/packages/react-native-codegen/src/generators/components/__tests__/__snapshots__/GeneratePropsJavaInterface-test.js.snap @@ -0,0 +1,293 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`GeneratePropsJavaInterface can generate fixture ARRAY_PROPS 1`] = ` +Map { + "ArrayPropsNativeComponentInterface.java" => " +package com.facebook.react.viewmanagers; + +import android.view.View; +import com.facebook.react.bridge.ReadableArray; + +public interface ArrayPropsNativeComponentInterface { + void setNames(T view, ReadableArray value);, + void setDisableds(T view, ReadableArray value);, + void setProgress(T view, ReadableArray value);, + void setRadii(T view, ReadableArray value);, + void setColors(T view, ReadableArray value);, + void setSrcs(T view, ReadableArray value);, + void setPoints(T view, ReadableArray value);, + void setSizes(T view, ReadableArray value); +} +", +} +`; + +exports[`GeneratePropsJavaInterface can generate fixture BOOLEAN_PROP 1`] = ` +Map { + "BooleanPropNativeComponentInterface.java" => " +package com.facebook.react.viewmanagers; + +import android.view.View; + +public interface BooleanPropNativeComponentInterface { + void setDisabled(T view, boolean value); +} +", +} +`; + +exports[`GeneratePropsJavaInterface can generate fixture COLOR_PROP 1`] = ` +Map { + "ColorPropNativeComponentInterface.java" => " +package com.facebook.react.viewmanagers; + +import android.view.View; + +public interface ColorPropNativeComponentInterface { + void setTintColor(T view, Integer value); +} +", +} +`; + +exports[`GeneratePropsJavaInterface can generate fixture COMMANDS 1`] = ` +Map { + "CommandNativeComponentInterface.java" => " +package com.facebook.react.viewmanagers; + +import android.view.View; + +public interface CommandNativeComponentInterface { + // No props +} +", +} +`; + +exports[`GeneratePropsJavaInterface can generate fixture ENUM_PROP 1`] = ` +Map { + "EnumPropsNativeComponentInterface.java" => " +package com.facebook.react.viewmanagers; + +import android.view.View; + +public interface EnumPropsNativeComponentInterface { + void setAlignment(T view, String value); +} +", +} +`; + +exports[`GeneratePropsJavaInterface can generate fixture EVENT_NESTED_OBJECT_PROPS 1`] = ` +Map { + "EventsNestedObjectNativeComponentInterface.java" => " +package com.facebook.react.viewmanagers; + +import android.view.View; + +public interface EventsNestedObjectNativeComponentInterface { + void setDisabled(T view, boolean value); +} +", +} +`; + +exports[`GeneratePropsJavaInterface can generate fixture EVENT_PROPS 1`] = ` +Map { + "EventsNativeComponentInterface.java" => " +package com.facebook.react.viewmanagers; + +import android.view.View; + +public interface EventsNativeComponentInterface { + void setDisabled(T view, boolean value); +} +", +} +`; + +exports[`GeneratePropsJavaInterface can generate fixture EVENTS_WITH_PAPER_NAME 1`] = ` +Map { + "InterfaceOnlyComponentInterface.java" => " +package com.facebook.react.viewmanagers; + +import android.view.View; + +public interface InterfaceOnlyComponentInterface { + // No props +} +", +} +`; + +exports[`GeneratePropsJavaInterface can generate fixture FLOAT_PROPS 1`] = ` +Map { + "FloatPropNativeComponentInterface.java" => " +package com.facebook.react.viewmanagers; + +import android.view.View; + +public interface FloatPropNativeComponentInterface { + void setBlurRadius(T view, Float value);, + void setBlurRadius2(T view, Float value);, + void setBlurRadius3(T view, Float value);, + void setBlurRadius4(T view, Float value);, + void setBlurRadius5(T view, Float value);, + void setBlurRadius6(T view, Float value); +} +", +} +`; + +exports[`GeneratePropsJavaInterface can generate fixture IMAGE_PROP 1`] = ` +Map { + "ImagePropNativeComponentInterface.java" => " +package com.facebook.react.viewmanagers; + +import android.view.View; +import com.facebook.react.bridge.ReadableMap; + +public interface ImagePropNativeComponentInterface { + void setThumbImage(T view, ReadableMap value); +} +", +} +`; + +exports[`GeneratePropsJavaInterface can generate fixture INTEGER_PROPS 1`] = ` +Map { + "IntegerPropNativeComponentInterface.java" => " +package com.facebook.react.viewmanagers; + +import android.view.View; + +public interface IntegerPropNativeComponentInterface { + void setProgress1(T view, int value);, + void setProgress2(T view, int value);, + void setProgress3(T view, int value); +} +", +} +`; + +exports[`GeneratePropsJavaInterface can generate fixture INTERFACE_ONLY 1`] = ` +Map { + "InterfaceOnlyComponentInterface.java" => " +package com.facebook.react.viewmanagers; + +import android.view.View; + +public interface InterfaceOnlyComponentInterface { + void setAccessibilityHint(T view, String value); +} +", +} +`; + +exports[`GeneratePropsJavaInterface can generate fixture MULTI_NATIVE_PROP 1`] = ` +Map { + "ImageColorPropNativeComponentInterface.java" => " +package com.facebook.react.viewmanagers; + +import android.view.View; +import com.facebook.react.bridge.ReadableMap; + +public interface ImageColorPropNativeComponentInterface { + void setThumbImage(T view, ReadableMap value);, + void setColor(T view, Integer value);, + void setThumbTintColor(T view, Integer value);, + void setPoint(T view, ReadableMap value); +} +", +} +`; + +exports[`GeneratePropsJavaInterface can generate fixture NO_PROPS_NO_EVENTS 1`] = ` +Map { + "NoPropsNoEventsComponentInterface.java" => " +package com.facebook.react.viewmanagers; + +import android.view.View; + +public interface NoPropsNoEventsComponentInterface { + // No props +} +", +} +`; + +exports[`GeneratePropsJavaInterface can generate fixture POINT_PROP 1`] = ` +Map { + "PointPropNativeComponentInterface.java" => " +package com.facebook.react.viewmanagers; + +import android.view.View; +import com.facebook.react.bridge.ReadableMap; + +public interface PointPropNativeComponentInterface { + void setStartPoint(T view, ReadableMap value); +} +", +} +`; + +exports[`GeneratePropsJavaInterface can generate fixture STRING_PROP 1`] = ` +Map { + "StringPropComponentInterface.java" => " +package com.facebook.react.viewmanagers; + +import android.view.View; + +public interface StringPropComponentInterface { + void setAccessibilityHint(T view, String value);, + void setAccessibilityRole(T view, String value); +} +", +} +`; + +exports[`GeneratePropsJavaInterface can generate fixture TWO_COMPONENTS_DIFFERENT_FILES 1`] = ` +Map { + "MultiFile1NativeComponentInterface.java" => " +package com.facebook.react.viewmanagers; + +import android.view.View; + +public interface MultiFile1NativeComponentInterface { + void setDisabled(T view, boolean value); +} +", + "MultiFile2NativeComponentInterface.java" => " +package com.facebook.react.viewmanagers; + +import android.view.View; + +public interface MultiFile2NativeComponentInterface { + void setDisabled(T view, boolean value); +} +", +} +`; + +exports[`GeneratePropsJavaInterface can generate fixture TWO_COMPONENTS_SAME_FILE 1`] = ` +Map { + "MultiComponent1NativeComponentInterface.java" => " +package com.facebook.react.viewmanagers; + +import android.view.View; + +public interface MultiComponent1NativeComponentInterface { + void setDisabled(T view, boolean value); +} +", + "MultiComponent2NativeComponentInterface.java" => " +package com.facebook.react.viewmanagers; + +import android.view.View; + +public interface MultiComponent2NativeComponentInterface { + void setDisabled(T view, boolean value); +} +", +} +`;