diff --git a/packages/react-native-codegen/src/CodegenSchema.js b/packages/react-native-codegen/src/CodegenSchema.js index 84a2d5e2fd9..9370d09dedd 100644 --- a/packages/react-native-codegen/src/CodegenSchema.js +++ b/packages/react-native-codegen/src/CodegenSchema.js @@ -264,6 +264,10 @@ export type NativeModuleSchema = $ReadOnly<{| aliases: NativeModuleAliasMap, spec: NativeModuleSpec, moduleNames: $ReadOnlyArray, + // Use for modules that are not used on other platforms. + // TODO: It's clearer to define `restrictedToPlatforms` instead, but + // `excludedPlatforms` is used here to be consistent with ComponentSchema. + excludedPlatforms?: $ReadOnlyArray, |}>; type NativeModuleSpec = $ReadOnly<{| diff --git a/packages/react-native-codegen/src/parsers/flow/index.js b/packages/react-native-codegen/src/parsers/flow/index.js index acb63944ce2..ab3a9d4a595 100644 --- a/packages/react-native-codegen/src/parsers/flow/index.js +++ b/packages/react-native-codegen/src/parsers/flow/index.js @@ -152,12 +152,12 @@ function buildSchema(contents: string, filename: ?string): SchemaType { if (filename === undefined || filename === null) { throw new Error('Filepath expected while parasing a module'); } - const moduleName = path.basename(filename).replace(/\.js$/, ''); + const hasteModuleName = path.basename(filename).replace(/\.js$/, ''); const regex = new RegExp(TURBO_MODULE_REGISTRY_REQUIRE_REGEX_STRING, 'g'); let match = regex.exec(contents); - const errorHeader = `Error while parsing Module '${moduleName}'`; + const errorHeader = `Error while parsing Module '${hasteModuleName}'`; if (match == null) { throw new Error( @@ -165,23 +165,23 @@ function buildSchema(contents: string, filename: ?string): SchemaType { ); } - const moduleRequires = []; + const moduleNames = []; while (match != null) { const resultGroups = match.groups; invariant( resultGroups != null, - `Couldn't parse TurboModuleRegistry.(get|getEnforcing) call in module '${moduleName}'.`, + `Couldn't parse TurboModuleRegistry.(get|getEnforcing) call in module '${hasteModuleName}'.`, ); - if (!moduleRequires.includes(resultGroups.nativeModuleName)) { - moduleRequires.push(resultGroups.nativeModuleName); + if (!moduleNames.includes(resultGroups.nativeModuleName)) { + moduleNames.push(resultGroups.nativeModuleName); } match = regex.exec(contents); } return wrapModuleSchema( - buildModuleSchema(moduleName, moduleRequires, types), - moduleName, + buildModuleSchema(hasteModuleName, moduleNames, types), + hasteModuleName, ); } } diff --git a/packages/react-native-codegen/src/parsers/flow/modules/__test_fixtures__/fixtures.js b/packages/react-native-codegen/src/parsers/flow/modules/__test_fixtures__/fixtures.js index 7a9b29a3d86..9ef58d4d097 100644 --- a/packages/react-native-codegen/src/parsers/flow/modules/__test_fixtures__/fixtures.js +++ b/packages/react-native-codegen/src/parsers/flow/modules/__test_fixtures__/fixtures.js @@ -27,7 +27,7 @@ import type {TurboModule} from '../RCTExport'; import * as TurboModuleRegistry from '../TurboModuleRegistry'; export interface Spec extends TurboModule { - // mo methods + // no methods } export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); @@ -497,6 +497,54 @@ export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); `; +const ANDROID_ONLY_NATIVE_MODULE = ` +/** + * 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 {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + // no methods +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModuleAndroid'); + +`; + +const IOS_ONLY_NATIVE_MODULE = ` +/** + * 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 {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + // no methods +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModuleIOS'); + +`; + module.exports = { NATIVE_MODULE_WITH_OBJECT_WITH_OBJECT_DEFINED_IN_FILE_AS_PROPERTY, NATIVE_MODULE_WITH_ARRAY_WITH_UNION_AND_TOUPLE, @@ -515,4 +563,6 @@ module.exports = { NATIVE_MODULE_WITH_BASIC_PARAM_TYPES, NATIVE_MODULE_WITH_CALLBACK, EMPTY_NATIVE_MODULE, + ANDROID_ONLY_NATIVE_MODULE, + IOS_ONLY_NATIVE_MODULE, }; diff --git a/packages/react-native-codegen/src/parsers/flow/modules/__tests__/__snapshots__/module-parser-snapshot-test.js.snap b/packages/react-native-codegen/src/parsers/flow/modules/__tests__/__snapshots__/module-parser-snapshot-test.js.snap index 9c38dc8fbfe..a8aa9c50d73 100644 --- a/packages/react-native-codegen/src/parsers/flow/modules/__tests__/__snapshots__/module-parser-snapshot-test.js.snap +++ b/packages/react-native-codegen/src/parsers/flow/modules/__tests__/__snapshots__/module-parser-snapshot-test.js.snap @@ -16,6 +16,26 @@ exports[`RN Codegen Flow Parser Fails with error message TWO_NATIVE_EXTENDING_TU exports[`RN Codegen Flow Parser Fails with error message TWO_NATIVE_MODULES_EXPORTED_WITH_DEFAULT 1`] = `"File neither contains a module declaration, nor a component declaration. For module declarations, please make sure your file has an InterfaceDeclaration extending TurboModule. For component declarations, please make sure your file has a default export calling the codegenNativeComponent(...) macro."`; +exports[`RN Codegen Flow Parser can generate fixture ANDROID_ONLY_NATIVE_MODULE 1`] = ` +"{ + 'modules': { + 'NativeSampleTurboModule': { + 'type': 'NativeModule', + 'aliases': {}, + 'spec': { + 'properties': [] + }, + 'moduleNames': [ + 'SampleTurboModuleAndroid' + ], + 'excludedPlatforms': [ + 'iOS' + ] + } + } +}" +`; + exports[`RN Codegen Flow Parser can generate fixture EMPTY_NATIVE_MODULE 1`] = ` "{ 'modules': { @@ -33,6 +53,26 @@ exports[`RN Codegen Flow Parser can generate fixture EMPTY_NATIVE_MODULE 1`] = ` }" `; +exports[`RN Codegen Flow Parser can generate fixture IOS_ONLY_NATIVE_MODULE 1`] = ` +"{ + 'modules': { + 'NativeSampleTurboModule': { + 'type': 'NativeModule', + 'aliases': {}, + 'spec': { + 'properties': [] + }, + 'moduleNames': [ + 'SampleTurboModuleIOS' + ], + 'excludedPlatforms': [ + 'android' + ] + } + } +}" +`; + exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_ALIASES 1`] = ` "{ 'modules': { diff --git a/packages/react-native-codegen/src/parsers/flow/modules/index.js b/packages/react-native-codegen/src/parsers/flow/modules/index.js index 652ac9f6180..7412e901375 100644 --- a/packages/react-native-codegen/src/parsers/flow/modules/index.js +++ b/packages/react-native-codegen/src/parsers/flow/modules/index.js @@ -363,7 +363,7 @@ function translateFunctionTypeAnnotation( } function buildPropertySchema( - moduleName: string, + hasteModuleName: string, // TODO(T71778680): This is an ObjectTypeProperty containing either: // - a FunctionTypeAnnotation or GenericTypeAnnotation // - a NullableTypeAnnoation containing a FunctionTypeAnnotation or GenericTypeAnnotation @@ -390,13 +390,13 @@ function buildPropertySchema( optional: property.optional, typeAnnotation: wrapNullable( nullable, - translateFunctionTypeAnnotation(moduleName, value, types, aliasMap), + translateFunctionTypeAnnotation(hasteModuleName, value, types, aliasMap), ), }; } function buildModuleSchema( - moduleName: string, + hasteModuleName: string, moduleNames: $ReadOnlyArray, types: TypeDeclarationMap, ): NativeModuleSchema { @@ -424,6 +424,20 @@ function buildModuleSchema( "Nativemodule interface must be called 'Spec'", ); + // Some module names use platform suffix to indicate platform-exclusive modules. + // Eventually this should be made explicit in the Flow type itself. + // Also check the hasteModuleName for platform suffix. + // Note: this shape is consistent with ComponentSchema. + const excludedPlatforms = []; + const namesToValidate = [...moduleNames, hasteModuleName]; + namesToValidate.forEach(name => { + if (name.endsWith('Android')) { + excludedPlatforms.push('iOS'); + } else if (name.endsWith('IOS')) { + excludedPlatforms.push('android'); + } + }); + const declaration = types[moduleInterfaceName]; return (declaration.body.properties: $ReadOnlyArray<$FlowFixMe>) .filter(property => property.type === 'ObjectTypeProperty') @@ -432,7 +446,7 @@ function buildModuleSchema( return { aliasMap: aliasMap, propertySchema: buildPropertySchema( - moduleName, + hasteModuleName, property, types, aliasMap, @@ -448,6 +462,7 @@ function buildModuleSchema( properties: [...moduleSchema.spec.properties, propertySchema], }, moduleNames: moduleSchema.moduleNames, + excludedPlatforms: moduleSchema.excludedPlatforms, }; }, { @@ -455,6 +470,8 @@ function buildModuleSchema( aliases: {}, spec: {properties: []}, moduleNames: moduleNames, + excludedPlatforms: + excludedPlatforms.length !== 0 ? [...excludedPlatforms] : undefined, }, ); }