From fe65af35616efb8bfbbf367cd27609313d47a168 Mon Sep 17 00:00:00 2001 From: Riccardo Cipolleschi Date: Tue, 13 Feb 2024 01:20:33 -0800 Subject: [PATCH] Generate code to allow for linking custom Module that conforms to some protocols (#42923) Summary: This Diff implement the logic to: - Read some lists of class names provided by libraries that conforms to some protocols we defined as extension points. - Generate a provider in the React-Codegen podspec, whose code lives alongside the app code. - Glue the app and the generated code together, allowing to link custom protocols ## Changelog [iOS][Added] - Allow libraries to provide module which conforms to protocols meant to be extension points. Reviewed By: RSNara Differential Revision: D53441411 --- .../Libraries/AppDelegate/RCTAppSetupUtils.mm | 61 ++++++++++++++++-- .../AppDelegate/React-RCTAppDelegate.podspec | 1 + .../codegen/generate-artifacts-executor.js | 64 +++++++++++++++++++ ...lesConformingToProtocolsProviderH.template | 18 ++++++ ...esConformingToProtocolsProviderMM.template | 33 ++++++++++ 5 files changed, 170 insertions(+), 7 deletions(-) create mode 100644 packages/react-native/scripts/codegen/templates/RCTModulesConformingToProtocolsProviderH.template create mode 100644 packages/react-native/scripts/codegen/templates/RCTModulesConformingToProtocolsProviderMM.template diff --git a/packages/react-native/Libraries/AppDelegate/RCTAppSetupUtils.mm b/packages/react-native/Libraries/AppDelegate/RCTAppSetupUtils.mm index 865cac04ff4a5b..9201d6af02e793 100644 --- a/packages/react-native/Libraries/AppDelegate/RCTAppSetupUtils.mm +++ b/packages/react-native/Libraries/AppDelegate/RCTAppSetupUtils.mm @@ -27,6 +27,17 @@ // jsinspector-modern #import +#if __has_include() +#define USE_OSS_CODEGEN 1 +#import +#elif __has_include() +#define USE_OSS_CODEGEN 1 +#import +#else +// Meta internal system do not generate the RCTModulesConformingToProtocolsProvider.h file +#define USE_OSS_CODEGEN 0 +#endif + void RCTAppSetupPrepareApp(UIApplication *application, BOOL turboModuleEnabled) { RCTEnableTurboModule(turboModuleEnabled); @@ -66,23 +77,59 @@ void RCTAppSetupPrepareApp( id RCTAppSetupDefaultModuleFromClass(Class moduleClass) { + // private block used to filter out modules depending on protocol conformance + NSArray * (^extractModuleConformingToProtocol)(RCTModuleRegistry *, Protocol *) = + ^NSArray *(RCTModuleRegistry *moduleRegistry, Protocol *protocol) + { + NSArray *classNames = @[]; + +#if USE_OSS_CODEGEN + if (protocol == @protocol(RCTImageURLLoader)) { + classNames = [RCTModulesConformingToProtocolsProvider imageURLLoaderClassNames]; + } else if (protocol == @protocol(RCTImageDataDecoder)) { + classNames = [RCTModulesConformingToProtocolsProvider imageDataDecoderClassNames]; + } else if (protocol == @protocol(RCTURLRequestHandler)) { + classNames = [RCTModulesConformingToProtocolsProvider URLRequestHandlerClassNames]; + } +#endif + + NSMutableArray *modules = [NSMutableArray new]; + + for (NSString *className in classNames) { + const char *cModuleName = [className cStringUsingEncoding:NSUTF8StringEncoding]; + id moduleFromLibrary = [moduleRegistry moduleForName:cModuleName]; + if (![moduleFromLibrary conformsToProtocol:protocol]) { + continue; + } + [modules addObject:moduleFromLibrary]; + } + return modules; + }; + // Set up the default RCTImageLoader and RCTNetworking modules. if (moduleClass == RCTImageLoader.class) { return [[moduleClass alloc] initWithRedirectDelegate:nil loadersProvider:^NSArray> *(RCTModuleRegistry *moduleRegistry) { - return @[ [RCTBundleAssetImageLoader new] ]; + NSArray *imageURLLoaderModules = + extractModuleConformingToProtocol(moduleRegistry, @protocol(RCTImageURLLoader)); + + return [@[ [RCTBundleAssetImageLoader new] ] arrayByAddingObjectsFromArray:imageURLLoaderModules]; } decodersProvider:^NSArray> *(RCTModuleRegistry *moduleRegistry) { - return @[ [RCTGIFImageDecoder new] ]; + NSArray *imageDataDecoder = extractModuleConformingToProtocol(moduleRegistry, @protocol(RCTImageDataDecoder)); + return [@[ [RCTGIFImageDecoder new] ] arrayByAddingObjectsFromArray:imageDataDecoder]; }]; } else if (moduleClass == RCTNetworking.class) { return [[moduleClass alloc] initWithHandlersProvider:^NSArray> *(RCTModuleRegistry *moduleRegistry) { - return [NSArray arrayWithObjects:[RCTHTTPRequestHandler new], - [RCTDataRequestHandler new], - [RCTFileRequestHandler new], - [moduleRegistry moduleForName:"BlobModule"], - nil]; + NSArray *URLRequestHandlerModules = + extractModuleConformingToProtocol(moduleRegistry, @protocol(RCTURLRequestHandler)); + return [@[ + [RCTHTTPRequestHandler new], + [RCTDataRequestHandler new], + [RCTFileRequestHandler new], + [moduleRegistry moduleForName:"BlobModule"], + ] arrayByAddingObjectsFromArray:URLRequestHandlerModules]; }]; } // No custom initializer here. diff --git a/packages/react-native/Libraries/AppDelegate/React-RCTAppDelegate.podspec b/packages/react-native/Libraries/AppDelegate/React-RCTAppDelegate.podspec index 47eaade4f414f0..a7552bf37857a0 100644 --- a/packages/react-native/Libraries/AppDelegate/React-RCTAppDelegate.podspec +++ b/packages/react-native/Libraries/AppDelegate/React-RCTAppDelegate.podspec @@ -76,6 +76,7 @@ Pod::Spec.new do |s| s.dependency "React-RCTImage" s.dependency "React-CoreModules" s.dependency "React-nativeconfig" + s.dependency "React-Codegen" add_dependency(s, "ReactCommon", :subspec => "turbomodule/core", :additional_framework_paths => ["react/nativemodule/core"]) add_dependency(s, "React-NativeModulesApple") diff --git a/packages/react-native/scripts/codegen/generate-artifacts-executor.js b/packages/react-native/scripts/codegen/generate-artifacts-executor.js index f5bb104a76f7df..a3bf875c534c06 100644 --- a/packages/react-native/scripts/codegen/generate-artifacts-executor.js +++ b/packages/react-native/scripts/codegen/generate-artifacts-executor.js @@ -60,6 +60,22 @@ const CORE_LIBRARIES_WITH_OUTPUT_FOLDER = { }; const REACT_NATIVE = 'react-native'; +const MODULES_PROTOCOLS_H_TEMPLATE_PATH = path.join( + REACT_NATIVE_PACKAGE_ROOT_FOLDER, + 'scripts', + 'codegen', + 'templates', + 'RCTModulesConformingToProtocolsProviderH.template', +); + +const MODULES_PROTOCOLS_MM_TEMPLATE_PATH = path.join( + REACT_NATIVE_PACKAGE_ROOT_FOLDER, + 'scripts', + 'codegen', + 'templates', + 'RCTModulesConformingToProtocolsProviderMM.template', +); + // HELPERS function pkgJsonIncludesGeneratedCode(pkgJson) { @@ -503,6 +519,52 @@ function findCodegenEnabledLibraries(pkgJson, projectRoot) { } } +function generateCustomURLHandlers(libraries, outputDir) { + const customImageURLLoaderClasses = libraries + .flatMap( + library => + library?.config?.ios?.modulesConformingToProtocol?.RCTImageURLLoader, + ) + .filter(Boolean) + .map(className => `@"${className}"`) + .join(',\n\t\t'); + + const customImageDataDecoderClasses = libraries + .flatMap( + library => + library?.config?.ios?.modulesConformingToProtocol?.RCTImageDataDecoder, + ) + .filter(Boolean) + .map(className => `@"${className}"`) + .join(',\n\t\t'); + + const customURLHandlerClasses = libraries + .flatMap( + library => + library?.config?.ios?.modulesConformingToProtocol?.RCTURLRequestHandler, + ) + .filter(Boolean) + .map(className => `@"${className}"`) + .join(',\n\t\t'); + + const template = fs.readFileSync(MODULES_PROTOCOLS_MM_TEMPLATE_PATH, 'utf8'); + const finalMMFile = template + .replace(/{imageURLLoaderClassNames}/, customImageURLLoaderClasses) + .replace(/{imageDataDecoderClassNames}/, customImageDataDecoderClasses) + .replace(/{requestHandlersClassNames}/, customURLHandlerClasses); + + fs.writeFileSync( + path.join(outputDir, 'RCTModulesConformingToProtocolsProvider.mm'), + finalMMFile, + ); + + const templateH = fs.readFileSync(MODULES_PROTOCOLS_H_TEMPLATE_PATH, 'utf8'); + fs.writeFileSync( + path.join(outputDir, 'RCTModulesConformingToProtocolsProvider.h'), + templateH, + ); +} + // It removes all the empty files and empty folders // it finds, starting from `filepath`, recursively. // @@ -615,6 +677,8 @@ function execute(projectRoot, targetPlatform, baseOutputPath) { createComponentProvider(schemas, supportedApplePlatforms); } + + generateCustomURLHandlers(libraries, outputPath); cleanupEmptyFilesAndFolders(outputPath); } } catch (err) { diff --git a/packages/react-native/scripts/codegen/templates/RCTModulesConformingToProtocolsProviderH.template b/packages/react-native/scripts/codegen/templates/RCTModulesConformingToProtocolsProviderH.template new file mode 100644 index 00000000000000..10eb8489175524 --- /dev/null +++ b/packages/react-native/scripts/codegen/templates/RCTModulesConformingToProtocolsProviderH.template @@ -0,0 +1,18 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@interface RCTModulesConformingToProtocolsProvider: NSObject + ++(NSArray *)imageURLLoaderClassNames; + ++(NSArray *)imageDataDecoderClassNames; + ++(NSArray *)URLRequestHandlerClassNames; + +@end diff --git a/packages/react-native/scripts/codegen/templates/RCTModulesConformingToProtocolsProviderMM.template b/packages/react-native/scripts/codegen/templates/RCTModulesConformingToProtocolsProviderMM.template new file mode 100644 index 00000000000000..1db7f6cb57a030 --- /dev/null +++ b/packages/react-native/scripts/codegen/templates/RCTModulesConformingToProtocolsProviderMM.template @@ -0,0 +1,33 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "RCTModulesConformingToProtocolsProvider.h" + +@implementation RCTModulesConformingToProtocolsProvider + ++(NSArray *)imageURLLoaderClassNames +{ + return @[ + {imageURLLoaderClassNames} + ]; +} + ++(NSArray *)imageDataDecoderClassNames +{ + return @[ + {imageDataDecoderClassNames} + ]; +} + ++(NSArray *)URLRequestHandlerClassNames +{ + return @[ + {requestHandlersClassNames} + ]; +} + +@end