Generate RCTThirdPartyComponentProvider (#47518)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/47518

This change reintroduce the generation of the `RCTThirdPartyComponentProvider` but in the right place and with the right patterns.

1. We are generating it in the user space, not in the node_modules (fixes the circular dependency)
2. We are not using weak function signature that have to be implicitly linked to some symbols found during compilation

The change needs to crawl the folder to retrieve the information it needs. We need to implement it this way not to be breaking with respect of the current implementation.

The assumption is that components have a function in their `.mm` file with this shape:
```objc
Class<RCTComponentViewProtocol> <componentName>Cls(void)
{
  return <ComponentViewClass>.class;
}
```
I verified on GH that all the libraries out there follow this pattern.

A better approach will let library owner to specify the association of `componentName, componentClass` in the `codegenConfig`.

We will implement that as the next step and we will support both for some versions for backward compatibility.

## Changelog
[iOS][Changed] - Change how components automatically register

Reviewed By: dmytrorykun

Differential Revision: D65614347

fbshipit-source-id: a378b8bc31c1ab3d49552f2f6a4c86c3b578746b
This commit is contained in:
Riccardo Cipolleschi 2024-11-12 07:38:03 -08:00 committed by Facebook GitHub Bot
parent 60b9d3d89e
commit 8becc2514d
7 changed files with 184 additions and 24 deletions

View File

@ -34,6 +34,14 @@
#endif
#import <react/nativemodule/defaults/DefaultTurboModules.h>
#if __has_include(<ReactCodegen/RCTThirdPartyComponentsProvider.h>)
#define USE_OSS_CODEGEN 1
#import <ReactCodegen/RCTThirdPartyComponentsProvider.h>
#else
// Meta internal system do not generate the RCTModulesConformingToProtocolsProvider.h file
#define USE_OSS_CODEGEN 0
#endif
using namespace facebook::react;
@interface RCTAppDelegate () <RCTComponentViewFactoryComponentProvider, RCTHostDelegate>
@ -235,7 +243,11 @@ using namespace facebook::react;
- (NSDictionary<NSString *, Class<RCTComponentViewProtocol>> *)thirdPartyFabricComponents
{
#if USE_OSS_CODEGEN
return [RCTThirdPartyComponentsProvider thirdPartyFabricComponents];
#else
return @{};
#endif
}
- (RCTRootViewFactory *)createRCTRootViewFactory

View File

@ -76,6 +76,7 @@ Pod::Spec.new do |s|
s.dependency "React-nativeconfig"
s.dependency "React-RCTFBReactNativeSpec"
s.dependency "React-defaultsnativemodule"
s.dependency "ReactCodegen"
add_dependency(s, "ReactCommon", :subspec => "turbomodule/core", :additional_framework_paths => ["react/nativemodule/core"])
add_dependency(s, "React-NativeModulesApple")

View File

@ -121,7 +121,8 @@ class CodegenUtils
'source_files' => "**/*.{h,mm,cpp}",
'pod_target_xcconfig' => {
"HEADER_SEARCH_PATHS" => header_search_paths.join(' '),
"FRAMEWORK_SEARCH_PATHS" => framework_search_paths
"FRAMEWORK_SEARCH_PATHS" => framework_search_paths,
"OTHER_CPLUSPLUSFLAGS" => "$(inherited) #{folly_compiler_flags} #{boost_compiler_flags}",
},
'dependencies': {
"React-jsiexecutor": [],

View File

@ -86,6 +86,22 @@ const MODULES_PROTOCOLS_MM_TEMPLATE_PATH = path.join(
'RCTModulesConformingToProtocolsProviderMM.template',
);
const THIRD_PARTY_COMPONENTS_H_TEMPLATE_PATH = path.join(
REACT_NATIVE_PACKAGE_ROOT_FOLDER,
'scripts',
'codegen',
'templates',
'RCTThirdPartyComponentsProviderH.template',
);
const THIRD_PARTY_COMPONENTS_MM_TEMPLATE_PATH = path.join(
REACT_NATIVE_PACKAGE_ROOT_FOLDER,
'scripts',
'codegen',
'templates',
'RCTThirdPartyComponentsProviderMM.template',
);
const codegenLog = (text, info = false) => {
// ANSI escape codes for colors and formatting
const reset = '\x1b[0m';
@ -541,28 +557,6 @@ function generateNativeCode(
});
}
function rootCodegenTargetNeedsThirdPartyComponentProvider(pkgJson, platform) {
return !pkgJsonIncludesGeneratedCode(pkgJson) && platform === 'ios';
}
function dependencyNeedsThirdPartyComponentProvider(
schemaInfo,
platform,
appCodegenConfigSpec,
) {
// Filter the react native core library out.
// In the future, core library and third party library should
// use the same way to generate/register the fabric components.
// We also have to filter out the the components defined in the app
// because the RCTThirdPartyComponentProvider is generated inside Fabric,
// which lives in a different target from the app and it has no visibility over
// the symbols defined in the app.
return (
!isReactNativeCoreLibrary(schemaInfo.library.config.name, platform) &&
schemaInfo.library.config.name !== appCodegenConfigSpec
);
}
function mustGenerateNativeCode(includeLibraryPath, schemaInfo) {
// If library's 'codegenConfig' sets 'includesGeneratedCode' to 'true',
// then we assume that native code is shipped with the library,
@ -634,6 +628,111 @@ function generateCustomURLHandlers(libraries, outputDir) {
);
}
function generateRCTThirdPartyComponents(libraries, outputDir) {
fs.mkdirSync(outputDir, {recursive: true});
// Generate Header File
codegenLog('Generating RCTThirdPartyComponentsProvider.h');
const templateH = fs.readFileSync(
THIRD_PARTY_COMPONENTS_H_TEMPLATE_PATH,
'utf8',
);
const finalPathH = path.join(outputDir, 'RCTThirdPartyComponentsProvider.h');
fs.writeFileSync(finalPathH, templateH);
codegenLog(`Generated artifact: ${finalPathH}`);
codegenLog('Generating RCTThirdPartyComponentsProvider.mm');
let componentsInLibraries = {};
libraries.forEach(({config, libraryPath}) => {
if (isReactNativeCoreLibrary(config.name) || config.type === 'modules') {
return;
}
const libraryName = JSON.parse(
fs.readFileSync(path.join(libraryPath, 'package.json')),
).name;
codegenLog(`Crawling ${libraryName} library for components`);
// crawl all files and subdirectories for file with the ".mm" extension
const files = findFilesWithExtension(libraryPath, '.mm');
componentsInLibraries[libraryName] = files
.flatMap(file => findRCTComponentViewProtocolClass(file))
.filter(Boolean);
});
const thirdPartyComponentsMapping = Object.keys(componentsInLibraries)
.flatMap(library => {
const components = componentsInLibraries[library];
return components.map(({componentName, className}) => {
return `\t\t@"${componentName}": NSClassFromString(@"${className}"), // ${library}`;
});
})
.join('\n');
// Generate implementation file
const templateMM = fs
.readFileSync(THIRD_PARTY_COMPONENTS_MM_TEMPLATE_PATH, 'utf8')
.replace(/{thirdPartyComponentsMapping}/, thirdPartyComponentsMapping);
const finalPathMM = path.join(
outputDir,
'RCTThirdPartyComponentsProvider.mm',
);
fs.writeFileSync(finalPathMM, templateMM);
codegenLog(`Generated artifact: ${finalPathMM}`);
}
// Given a path, return the paths of all the files with extension .mm in
// the path dir and all its subdirectories.
function findFilesWithExtension(filePath, extension) {
const files = [];
const dir = fs.readdirSync(filePath);
dir.forEach(file => {
const absolutePath = path.join(filePath, file);
if (
fs.existsSync(absolutePath) &&
fs.statSync(absolutePath).isDirectory()
) {
files.push(...findFilesWithExtension(absolutePath, extension));
} else if (file.endsWith(extension)) {
files.push(absolutePath);
}
});
return files;
}
// Given a filepath, read the file and look for a string that starts with 'Class<RCTComponentViewProtocol> '
// and ends with 'Cls(void)'. Return the string between the two.
function findRCTComponentViewProtocolClass(filepath) {
const fileContent = fs.readFileSync(filepath, 'utf8');
const regex = /Class<RCTComponentViewProtocol> (.*)Cls\(/;
const match = fileContent.match(regex);
if (match) {
const componentName = match[1];
// split the file by \n
// remove all the lines before the one that matches the regex above
// find the first return statement after that that ends with .class
// return what's between return and `.class`
const lines = fileContent.split('\n');
const signatureIndex = lines.findIndex(line => regex.test(line));
const returnRegex = /return (.*)\.class/;
const classNameMatch = String(lines.slice(signatureIndex)).match(
returnRegex,
);
if (classNameMatch) {
const className = classNameMatch[1];
codegenLog(`Match found ${componentName} -> ${className}`);
return {
componentName,
className,
};
}
console.warn(
`Could not find class name for component ${componentName}. Register it manually`,
);
return null;
}
return null;
}
// It removes all the empty files and empty folders
// it finds, starting from `filepath`, recursively.
//
@ -764,6 +863,7 @@ function execute(projectRoot, targetPlatform, baseOutputPath) {
platform,
);
generateRCTThirdPartyComponents(libraries, outputPath);
generateCustomURLHandlers(libraries, outputPath);
cleanupEmptyFilesAndFolders(outputPath);

View File

@ -0,0 +1,16 @@
/*
* 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 <Foundation/Foundation.h>
@protocol RCTComponentViewProtocol;
@interface RCTThirdPartyComponentsProvider: NSObject
+ (NSDictionary<NSString *, Class<RCTComponentViewProtocol>> *)thirdPartyFabricComponents;
@end

View File

@ -0,0 +1,23 @@
/*
* 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 <Foundation/Foundation.h>
#import "RCTThirdPartyComponentsProvider.h"
#import <React/RCTComponentViewProtocol.h>
@implementation RCTThirdPartyComponentsProvider
+ (NSDictionary<NSString *, Class<RCTComponentViewProtocol>> *)thirdPartyFabricComponents
{
return @{
{thirdPartyComponentsMapping}
};
}
@end

View File

@ -149,7 +149,14 @@ static NSString *kBundlePath = @"js/RNTesterApp.ios";
#ifndef RN_DISABLE_OSS_PLUGIN_HEADER
- (nonnull NSDictionary<NSString *, Class<RCTComponentViewProtocol>> *)thirdPartyFabricComponents
{
return @{@"RNTMyNativeView" : RNTMyNativeViewComponentView.class};
#if USE_OSS_CODEGEN
return [super thirdPartyFabricComponents].mutableCopy;
#else
NSMutableDictionary *dict = [super thirdPartyFabricComponents].mutableCopy;
dict[@"RNTMyNativeView"] = NSClassFromString(@"RNTMyNativeViewComponentView");
dict[@"SampleNativeComponent"] = NSClassFromString(@"RCTSampleNativeComponentComponentView");
return dict;
#endif
}
#endif