mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
codegen: complete Android Java spec support in the JS generator
Summary: This builds on the previous commit and complete all current NativeModule spec support for React Native Android: * method param (nullable) typing * promise return support * sync method * getConstants() special Android runtime validation Changelog: [Internal] Reviewed By: mdvacca Differential Revision: D22862422 fbshipit-source-id: abc6d46fb8ce5863677910de1acc8bb6a927e7da
This commit is contained in:
committed by
Facebook GitHub Bot
parent
77e0ba2fb0
commit
e3c0f6b026
+247
-16
@@ -11,13 +11,13 @@
|
||||
'use strict';
|
||||
|
||||
import type {
|
||||
FunctionTypeAnnotationParamTypeAnnotation,
|
||||
FunctionTypeAnnotationParam,
|
||||
FunctionTypeAnnotationReturn,
|
||||
NativeModuleShape,
|
||||
TypeAliasTypeAnnotation,
|
||||
NativeModuleMethodTypeShape,
|
||||
ObjectTypeAliasTypeShape,
|
||||
SchemaType,
|
||||
} from '../../CodegenSchema';
|
||||
const {getTypeAliasTypeAnnotation} = require('./Utils');
|
||||
|
||||
type FilesOutput = Map<string, string>;
|
||||
|
||||
@@ -45,20 +45,192 @@ public abstract class ::_CLASSNAME_:: extends ReactContextBaseJavaModule impleme
|
||||
}
|
||||
`;
|
||||
|
||||
function getImportList(nativeModule: NativeModuleShape): Set<string> {
|
||||
const imports: Set<string> = new Set();
|
||||
function translateFunctionParamToJavaType(
|
||||
param: FunctionTypeAnnotationParam,
|
||||
createErrorMessage: (typeName: string) => string,
|
||||
aliases: $ReadOnly<{[aliasName: string]: ObjectTypeAliasTypeShape, ...}>,
|
||||
imports: Set<string>,
|
||||
): string {
|
||||
const {nullable, typeAnnotation} = param;
|
||||
|
||||
// Always required.
|
||||
imports.add('com.facebook.react.bridge.ReactApplicationContext');
|
||||
imports.add('com.facebook.react.bridge.ReactContextBaseJavaModule');
|
||||
imports.add('com.facebook.react.bridge.ReactMethod');
|
||||
imports.add('com.facebook.react.bridge.ReactModuleWithSpec');
|
||||
imports.add('com.facebook.react.turbomodule.core.interfaces.TurboModule');
|
||||
function wrapIntoNullableIfNeeded(generatedType: string) {
|
||||
if (nullable) {
|
||||
imports.add('javax.annotation.Nullable');
|
||||
return `@Nullable ${generatedType}`;
|
||||
}
|
||||
return generatedType;
|
||||
}
|
||||
|
||||
return imports;
|
||||
const realTypeAnnotation =
|
||||
typeAnnotation.type === 'TypeAliasTypeAnnotation'
|
||||
? getTypeAliasTypeAnnotation(typeAnnotation.name, aliases)
|
||||
: typeAnnotation;
|
||||
switch (realTypeAnnotation.type) {
|
||||
case 'ReservedFunctionValueTypeAnnotation':
|
||||
switch (realTypeAnnotation.name) {
|
||||
case 'RootTag':
|
||||
return nullable ? 'Double' : 'double';
|
||||
default:
|
||||
(realTypeAnnotation.name: empty);
|
||||
throw new Error(createErrorMessage(realTypeAnnotation.name));
|
||||
}
|
||||
case 'StringTypeAnnotation':
|
||||
return wrapIntoNullableIfNeeded('String');
|
||||
case 'NumberTypeAnnotation':
|
||||
case 'FloatTypeAnnotation':
|
||||
case 'Int32TypeAnnotation':
|
||||
return nullable ? 'Double' : 'double';
|
||||
case 'BooleanTypeAnnotation':
|
||||
return nullable ? 'Boolean' : 'boolean';
|
||||
case 'ObjectTypeAnnotation':
|
||||
imports.add('com.facebook.react.bridge.ReadableMap');
|
||||
if (typeAnnotation.type === 'TypeAliasTypeAnnotation') {
|
||||
// No class alias for args, so it still falls under ReadableMap.
|
||||
return 'ReadableMap';
|
||||
}
|
||||
return 'ReadableMap';
|
||||
case 'GenericObjectTypeAnnotation':
|
||||
// Treat this the same as ObjectTypeAnnotation for now.
|
||||
imports.add('com.facebook.react.bridge.ReadableMap');
|
||||
return 'ReadableMap';
|
||||
case 'ArrayTypeAnnotation':
|
||||
imports.add('com.facebook.react.bridge.ReadableArray');
|
||||
return 'ReadableArray';
|
||||
case 'FunctionTypeAnnotation':
|
||||
imports.add('com.facebook.react.bridge.Callback');
|
||||
return 'Callback';
|
||||
default:
|
||||
throw new Error(createErrorMessage(realTypeAnnotation.type));
|
||||
}
|
||||
}
|
||||
|
||||
function translateFunctionReturnTypeToJavaType(
|
||||
returnTypeAnnotation: FunctionTypeAnnotationReturn,
|
||||
createErrorMessage: (typeName: string) => string,
|
||||
imports: Set<string>,
|
||||
): string {
|
||||
const {nullable} = returnTypeAnnotation;
|
||||
|
||||
function wrapIntoNullableIfNeeded(generatedType: string) {
|
||||
if (nullable) {
|
||||
imports.add('javax.annotation.Nullable');
|
||||
return `@Nullable ${generatedType}`;
|
||||
}
|
||||
return generatedType;
|
||||
}
|
||||
|
||||
// TODO: Support aliased return type. This doesn't exist in React Native Android yet.
|
||||
switch (returnTypeAnnotation.type) {
|
||||
case 'ReservedFunctionValueTypeAnnotation':
|
||||
switch (returnTypeAnnotation.name) {
|
||||
case 'RootTag':
|
||||
return nullable ? 'Double' : 'double';
|
||||
default:
|
||||
(returnTypeAnnotation.name: empty);
|
||||
throw new Error(createErrorMessage(returnTypeAnnotation.name));
|
||||
}
|
||||
case 'VoidTypeAnnotation':
|
||||
case 'GenericPromiseTypeAnnotation':
|
||||
return 'void';
|
||||
case 'StringTypeAnnotation':
|
||||
return wrapIntoNullableIfNeeded('String');
|
||||
case 'NumberTypeAnnotation':
|
||||
case 'FloatTypeAnnotation':
|
||||
case 'Int32TypeAnnotation':
|
||||
return nullable ? 'Double' : 'double';
|
||||
case 'BooleanTypeAnnotation':
|
||||
return nullable ? 'Boolean' : 'boolean';
|
||||
case 'ObjectTypeAnnotation':
|
||||
imports.add('com.facebook.react.bridge.WritableMap');
|
||||
return 'WritableMap';
|
||||
case 'GenericObjectTypeAnnotation':
|
||||
imports.add('com.facebook.react.bridge.WritableMap');
|
||||
return 'WritableMap';
|
||||
case 'ArrayTypeAnnotation':
|
||||
imports.add('com.facebook.react.bridge.WritableArray');
|
||||
return 'WritableArray';
|
||||
default:
|
||||
throw new Error(createErrorMessage(returnTypeAnnotation.type));
|
||||
}
|
||||
}
|
||||
|
||||
// Build special-cased runtime check for getConstants().
|
||||
function buildGetConstantsMethod(
|
||||
method: NativeModuleMethodTypeShape,
|
||||
imports: Set<string>,
|
||||
): string {
|
||||
if (
|
||||
method.typeAnnotation.returnTypeAnnotation.type === 'ObjectTypeAnnotation'
|
||||
) {
|
||||
const requiredProps = [];
|
||||
const optionalProps = [];
|
||||
const rawProperties =
|
||||
method.typeAnnotation.returnTypeAnnotation.properties || [];
|
||||
rawProperties.forEach(p => {
|
||||
if (p.optional) {
|
||||
optionalProps.push(p.name);
|
||||
} else {
|
||||
requiredProps.push(p.name);
|
||||
}
|
||||
});
|
||||
if (requiredProps.length === 0 && optionalProps.length === 0) {
|
||||
// Nothing to validate during runtime.
|
||||
return '';
|
||||
}
|
||||
|
||||
imports.add('com.facebook.react.common.build.ReactBuildConfig');
|
||||
imports.add('java.util.Arrays');
|
||||
imports.add('java.util.HashSet');
|
||||
imports.add('java.util.Map');
|
||||
imports.add('java.util.Set');
|
||||
imports.add('javax.annotation.Nullable');
|
||||
|
||||
const requiredPropsFragment =
|
||||
requiredProps.length > 0
|
||||
? `Arrays.asList(
|
||||
${requiredProps
|
||||
.sort()
|
||||
.map(p => `"${p}"`)
|
||||
.join(',\n ')}
|
||||
)`
|
||||
: '';
|
||||
const optionalPropsFragment =
|
||||
optionalProps.length > 0
|
||||
? `Arrays.asList(
|
||||
${optionalProps
|
||||
.sort()
|
||||
.map(p => `"${p}"`)
|
||||
.join(',\n ')}
|
||||
)`
|
||||
: '';
|
||||
|
||||
return ` protected abstract Map<String, Object> getTypedExportedConstants();
|
||||
|
||||
@Override
|
||||
public final @Nullable Map<String, Object> getConstants() {
|
||||
Map<String, Object> constants = getTypedExportedConstants();
|
||||
if (ReactBuildConfig.DEBUG || ReactBuildConfig.IS_INTERNAL_BUILD) {
|
||||
Set<String> obligatoryFlowConstants = new HashSet<>(${requiredPropsFragment});
|
||||
Set<String> optionalFlowConstants = new HashSet<>(${optionalPropsFragment});
|
||||
Set<String> undeclaredConstants = new HashSet<>(constants.keySet());
|
||||
undeclaredConstants.removeAll(obligatoryFlowConstants);
|
||||
undeclaredConstants.removeAll(optionalFlowConstants);
|
||||
if (!undeclaredConstants.isEmpty()) {
|
||||
throw new IllegalStateException(String.format("Native Module Flow doesn't declare constants: %s", undeclaredConstants));
|
||||
}
|
||||
undeclaredConstants = obligatoryFlowConstants;
|
||||
undeclaredConstants.removeAll(constants.keySet());
|
||||
if (!undeclaredConstants.isEmpty()) {
|
||||
throw new IllegalStateException(String.format("Native Module doesn't fill in constants: %s", undeclaredConstants));
|
||||
}
|
||||
}
|
||||
return constants;
|
||||
}`;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
// TODO: complete this implementation
|
||||
module.exports = {
|
||||
generate(
|
||||
libraryName: string,
|
||||
@@ -67,7 +239,7 @@ module.exports = {
|
||||
): FilesOutput {
|
||||
const files = new Map();
|
||||
// TODO: Allow package configuration.
|
||||
const packageName = 'com.facebook.fbreact.specs';
|
||||
const packageName = 'com.facebook.fbreact.specs.beta';
|
||||
const nativeModules = Object.keys(schema.modules)
|
||||
.map(moduleName => {
|
||||
const modules = schema.modules[moduleName].nativeModules;
|
||||
@@ -84,15 +256,74 @@ module.exports = {
|
||||
const {aliases, properties} = nativeModules[name];
|
||||
const className = `Native${name}Spec`;
|
||||
|
||||
const imports: Set<string> = new Set([
|
||||
// Always required.
|
||||
'com.facebook.react.bridge.ReactApplicationContext',
|
||||
'com.facebook.react.bridge.ReactContextBaseJavaModule',
|
||||
'com.facebook.react.bridge.ReactMethod',
|
||||
'com.facebook.react.bridge.ReactModuleWithSpec',
|
||||
'com.facebook.react.turbomodule.core.interfaces.TurboModule',
|
||||
]);
|
||||
|
||||
const methods = properties.map(method => {
|
||||
const traversedArgs = method.typeAnnotation.params.map(param => {});
|
||||
if (method.name === 'getConstants') {
|
||||
return buildGetConstantsMethod(method, imports);
|
||||
}
|
||||
|
||||
// Handle return type
|
||||
const translatedReturnType = translateFunctionReturnTypeToJavaType(
|
||||
method.typeAnnotation.returnTypeAnnotation,
|
||||
typeName =>
|
||||
`Unsupported return type for method ${method.name}. Found: ${typeName}`,
|
||||
imports,
|
||||
);
|
||||
const returningPromise =
|
||||
method.typeAnnotation.returnTypeAnnotation.type ===
|
||||
'GenericPromiseTypeAnnotation';
|
||||
const isSyncMethod =
|
||||
method.typeAnnotation.returnTypeAnnotation.type !==
|
||||
'VoidTypeAnnotation' && !returningPromise;
|
||||
|
||||
// Handle method args
|
||||
const traversedArgs = method.typeAnnotation.params.map(param => {
|
||||
const translatedParam = translateFunctionParamToJavaType(
|
||||
param,
|
||||
typeName =>
|
||||
`Unsupported type for param "${param.name}" in ${method.name}. Found: ${typeName}`,
|
||||
aliases,
|
||||
imports,
|
||||
);
|
||||
return `${translatedParam} ${param.name}`;
|
||||
});
|
||||
|
||||
if (returningPromise) {
|
||||
// Promise return type requires an extra arg at the end.
|
||||
imports.add('com.facebook.react.bridge.Promise');
|
||||
traversedArgs.push('Promise promise');
|
||||
}
|
||||
|
||||
const methodJavaAnnotation = `@ReactMethod${
|
||||
isSyncMethod ? '(isBlockingSynchronousMethod = true)' : ''
|
||||
}`;
|
||||
return ` ${methodJavaAnnotation}
|
||||
public abstract ${translatedReturnType} ${method.name}(${traversedArgs.join(
|
||||
', ',
|
||||
)});`;
|
||||
});
|
||||
|
||||
files.set(
|
||||
`${className}.java`,
|
||||
moduleTemplate
|
||||
.replace(
|
||||
/::_IMPORTS_::/g,
|
||||
Array.from(imports)
|
||||
.sort()
|
||||
.map(p => `import ${p};`)
|
||||
.join('\n'),
|
||||
)
|
||||
.replace(/::_PACKAGENAME_::/g, packageName)
|
||||
.replace(/::_CLASSNAME_::/g, className),
|
||||
.replace(/::_CLASSNAME_::/g, className)
|
||||
.replace(/::_METHODS_::/g, methods.filter(m => !!m).join('\n\n')),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user