Skip to content

Commit

Permalink
Generate RCTThirdPartyComponentProvider (#47518)
Browse files Browse the repository at this point in the history
Summary:
Pull Request resolved: #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
  • Loading branch information
cipolleschi authored and facebook-github-bot committed Nov 12, 2024
1 parent 60b9d3d commit 8becc25
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 24 deletions.
12 changes: 12 additions & 0 deletions packages/react-native/Libraries/AppDelegate/RCTAppDelegate.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -235,7 +243,11 @@ - (Class)getModuleClassFromName:(const char *)name

- (NSDictionary<NSString *, Class<RCTComponentViewProtocol>> *)thirdPartyFabricComponents
{
#if USE_OSS_CODEGEN
return [RCTThirdPartyComponentsProvider thirdPartyFabricComponents];
#else
return @{};
#endif
}

- (RCTRootViewFactory *)createRCTRootViewFactory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
3 changes: 2 additions & 1 deletion packages/react-native/scripts/cocoapods/codegen_utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,8 @@ def get_react_codegen_spec(package_json_file, folly_version: get_folly_config()[
'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": [],
Expand Down
144 changes: 122 additions & 22 deletions packages/react-native/scripts/codegen/generate-artifacts-executor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.
//
Expand Down Expand Up @@ -764,6 +863,7 @@ function execute(projectRoot, targetPlatform, baseOutputPath) {
platform,
);

generateRCTThirdPartyComponents(libraries, outputPath);
generateCustomURLHandlers(libraries, outputPath);

cleanupEmptyFilesAndFolders(outputPath);
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
9 changes: 8 additions & 1 deletion packages/rn-tester/RNTester/AppDelegate.mm
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,14 @@ - (BOOL)bridgelessEnabled
#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

Expand Down

0 comments on commit 8becc25

Please sign in to comment.