diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9024300cb..bd1738b6d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,7 +39,7 @@ And then: ```sh cd /my/new/react-native/project/ -yarn link "@react-native-community/cli-platform-ios" "@react-native-community/cli-platform-android" "@react-native-community/cli" "@react-native-community/cli-server-api" "@react-native-community/cli-types" "@react-native-community/cli-tools" "@react-native-community/cli-debugger-ui" "@react-native-community/cli-hermes" "@react-native-community/cli-plugin-metro" +yarn link "@react-native-community/cli-platform-ios" "@react-native-community/cli-platform-android" "@react-native-community/cli" "@react-native-community/cli-server-api" "@react-native-community/cli-types" "@react-native-community/cli-tools" "@react-native-community/cli-debugger-ui" "@react-native-community/cli-hermes" "@react-native-community/cli-plugin-metro" "@react-native-community/cli-clean" ``` Once you're done with testing and you'd like to get back to regular setup, run `yarn unlink` instead of `yarn link` from above command. Then `yarn install --force`. diff --git a/docs/autolinking.md b/docs/autolinking.md index b6d84d8b4..4e763efd5 100644 --- a/docs/autolinking.md +++ b/docs/autolinking.md @@ -16,6 +16,7 @@ yarn react-native run-android That's it. No more editing build config files to use native code. Also, removing a library is similar to adding a library: + ```sh # uninstall yarn remove react-native-webview @@ -48,6 +49,10 @@ The [native_modules.gradle](https://github.com/react-native-community/cli/blob/m 1. At build time, before the build script is run: 1. A first Gradle plugin (in `settings.gradle`) runs `applyNativeModulesSettingsGradle` method. It uses the package metadata from `react-native config` to add Android projects. 1. A second Gradle plugin (in `app/build.gradle`) runs `applyNativeModulesAppBuildGradle` method. It creates a list of React Native packages to include in the generated `/android/build/generated/rn/src/main/java/com/facebook/react/PackageList.java` file. + 1. When the new architecture is turned on, the `generateNewArchitectureFiles` task is fired, generating `/android/build/generated/rn/src/main/jni` directory with the following files: + - `Android-rncli.mk` – creates a list of codegen'd libs. Used by the project's `Android.mk`. + - `rncli.cpp` – registers codegen'd Turbo Modules and Fabric component providers. Used by `MainApplicationModuleProvider.cpp` and `MainComponentsRegistry.cpp`. + - `rncli.h` - a header file for `rncli.cpp`. 1. At runtime, the list of React Native packages generated in step 1.2 is registered by `getPackages` method of `ReactNativeHost` in `MainApplication.java`. 1. You can optionally pass in an instance of `MainPackageConfig` when initializing `PackageList` if you want to override the default configuration of `MainReactPackage`. @@ -97,6 +102,27 @@ module.exports = { }; ``` +## How can I disable autolinking for new architecture (Fabric, TurboModules)? + +It happens that packages come with their own linking setup for the new architecture. To disable autolinking in such cases (currently `react-native-screens`, `react-native-safe-area-context`, `react-native-reanimated`, `react-native-gesture-handler`), update your `react-native.config.js`'s `dependencies` entry to look like this: + +```js +// react-native.config.js +module.exports = { + dependencies: { + 'fabric-or-tm-library': { + platforms: { + android: { + libraryName: null, + componentDescriptors: null, + androidMkPath: null, + }, + }, + }, + }, +}; +``` + ## How can I autolink a local library? We can leverage CLI configuration to make it "see" React Native libraries that are not part of our 3rd party dependencies. To do so, update your `react-native.config.js`'s `dependencies` entry to look like this: @@ -124,6 +150,7 @@ correct location and update them accordingly: - path to `native_modules.gradle` in your `android/app/build.gradle` Dependencies are only linked if they are listed in the package.json of the mobile workspace, where "react-native" dependency is defined. For example, with this file structure: + ``` /root /packages @@ -135,4 +162,5 @@ Dependencies are only linked if they are listed in the package.json of the mobil package.json <-- Dependencies here are ignored when auto-linking package.json ``` + In this example, if you add a package with native code as a dependency of `components`, you need to also add it as a dependency of `mobile` for auto-linking to work. diff --git a/docs/dependencies.md b/docs/dependencies.md index 30d574519..ef32d8389 100644 --- a/docs/dependencies.md +++ b/docs/dependencies.md @@ -55,6 +55,9 @@ type AndroidDependencyParams = { packageImportPath?: string; packageInstance?: string; buildTypes?: string[]; + libraryName?: string | null; + componentDescriptors?: string[] | null; + androidMkPath?: string | null; }; ``` @@ -119,3 +122,21 @@ An array of build variants or flavors which will include the dependency. If the A string that defines which method other than `implementation` do you want to use for autolinking inside `build.gradle` i.e: `'embed project(path: ":$dependencyName", configuration: "default")',` - `"dependencyName` will be replaced by the actual package's name. You can achieve the same result by directly defining this key per `dependency` _(without placeholder)_ and it will have higher priority than this option. + +#### platforms.android.libraryName + +> Note: Only applicable when new architecture is turned on. + +A string indicating your custom library name. By default it's taken from the `libraryName` variable in your library's `build.gradle`. + +#### platforms.android.componentDescriptors + +> Note: Only applicable when new architecture is turned on. + +An array of custom component descriptor strings. By default they're generated based on `codegenNativeComponent` calls. + +#### platforms.android.androidMkPath + +> Note: Only applicable when new architecture is turned on. + +A relative path to a custom _Android.mk_ file not registered by codegen. Relative to `sourceDir`. diff --git a/docs/platforms.md b/docs/platforms.md index 1d187ad37..053ada04f 100644 --- a/docs/platforms.md +++ b/docs/platforms.md @@ -118,5 +118,8 @@ type AndroidDependencyConfig = { packageInstance: string; dependencyConfiguration?: string; buildTypes: string[]; + libraryName?: string | null; + componentDescriptors?: string[] | null; + androidMkPath?: string | null; }; ``` diff --git a/packages/cli-config/src/schema.ts b/packages/cli-config/src/schema.ts index e1079bd6f..c777f273d 100644 --- a/packages/cli-config/src/schema.ts +++ b/packages/cli-config/src/schema.ts @@ -83,6 +83,9 @@ export const dependencyConfig = t packageInstance: t.string(), dependencyConfiguration: t.string(), buildTypes: t.array().items(t.string()).default([]), + libraryName: t.string().allow(null), + componentDescriptors: t.array().items(t.string()).allow(null), + androidMkPath: t.string().allow(null), }) .default({}), }) @@ -131,6 +134,9 @@ export const projectConfig = t packageInstance: t.string(), dependencyConfiguration: t.string(), buildTypes: t.array().items(t.string()).default([]), + libraryName: t.string().allow(null), + componentDescriptors: t.array().items(t.string()).allow(null), + androidMkPath: t.string().allow(null), }) .allow(null), }), diff --git a/packages/cli-types/src/android.ts b/packages/cli-types/src/android.ts index a74112da6..aa2a796f6 100644 --- a/packages/cli-types/src/android.ts +++ b/packages/cli-types/src/android.ts @@ -19,6 +19,9 @@ export type AndroidDependencyConfig = { packageInstance: string; dependencyConfiguration?: string; buildTypes: string[]; + libraryName?: string | null; + componentDescriptors?: string[] | null; + androidMkPath?: string | null; }; export type AndroidDependencyParams = { @@ -29,4 +32,7 @@ export type AndroidDependencyParams = { packageImportPath?: string; packageInstance?: string; buildTypes?: string[]; + libraryName?: string | null; + componentDescriptors?: string[] | null; + androidMkPath?: string | null; }; diff --git a/packages/platform-android/native_modules.gradle b/packages/platform-android/native_modules.gradle index 20c858ba5..3fa41853c 100644 --- a/packages/platform-android/native_modules.gradle +++ b/packages/platform-android/native_modules.gradle @@ -68,6 +68,67 @@ public class PackageList { } """ +def androidMkTemplate = """# This code was generated by [React Native CLI](https://www.npmjs.com/package/@react-native-community/cli) + +{{ libraryIncludes }} + +import-codegen-modules := \\ + {{ libraryModules }} +""" + +def rncliCppTemplate = """/** + * This code was generated by [React Native CLI](https://www.npmjs.com/package/@react-native-community/cli). + * + * Do not edit this file as changes may cause incorrect behavior and will be lost + * once the code is regenerated. + * + */ + +#include "rncli.h" +{{ rncliCppIncludes }} + +namespace facebook { +namespace react { + +std::shared_ptr rncli_ModuleProvider(const std::string moduleName, const JavaTurboModule::InitParams ¶ms) { +{{ rncliCppModuleProviders }} + return nullptr; +} + +void rncli_registerProviders(std::shared_ptr providerRegistry) { +{{ rncliCppComponentDescriptors }} + return; +} + +} // namespace react +} // namespace facebook +""" + +def rncliHTemplate = """/** + * This code was generated by [React Native CLI](https://www.npmjs.com/package/@react-native-community/cli). + * + * Do not edit this file as changes may cause incorrect behavior and will be lost + * once the code is regenerated. + * + */ + +#pragma once + +#include +#include +#include +#include + +namespace facebook { +namespace react { + +std::shared_ptr rncli_ModuleProvider(const std::string moduleName, const JavaTurboModule::InitParams ¶ms); +void rncli_registerProviders(std::shared_ptr providerRegistry); + +} // namespace react +} // namespace facebook +""" + class ReactNativeModules { private Logger logger private String packageName @@ -187,6 +248,91 @@ class ReactNativeModules { } } + void generateAndroidMkFile(File outputDir, String generatedFileName, String generatedFileContentsTemplate) { + ArrayList> packages = this.reactNativeModules + String packageName = this.packageName + String codegenLibPrefix = "libreact_codegen_" + String libraryIncludes = "" + String libraryModules = "" + + if (packages.size() > 0) { + libraryIncludes = packages.collect { + it.androidMkPath ? "include ${it.androidMkPath}" : null + }.minus(null).join('\n') + libraryModules = packages.collect { + it.libraryName ? "${codegenLibPrefix}${it.libraryName}" : null + }.minus(null).join(" \\\n ") + } + + String generatedFileContents = generatedFileContentsTemplate + .replace("{{ libraryIncludes }}", libraryIncludes) + .replace("{{ libraryModules }}", libraryModules) + + outputDir.mkdirs() + final FileTreeBuilder treeBuilder = new FileTreeBuilder(outputDir) + treeBuilder.file(generatedFileName).newWriter().withWriter { w -> + w << generatedFileContents + } + } + + void generateRncliCpp(File outputDir, String generatedFileName, String generatedFileContentsTemplate) { + ArrayList> packages = this.reactNativeModules + String rncliCppIncludes = "" + String rncliCppModuleProviders = "" + String rncliCppComponentDescriptors = "" + String codegenComponentDescriptorsHeaderFile = "ComponentDescriptors.h" + String codegenReactComponentsDir = "react/renderer/components" + + if (packages.size() > 0) { + rncliCppIncludes = packages.collect { + if (!it.libraryName) { + return null + } + def result = "#include <${it.libraryName}.h>" + if (it.componentDescriptors && it.componentDescriptors.size() > 0) { + result += "\n#include <${codegenReactComponentsDir}/${it.libraryName}/${codegenComponentDescriptorsHeaderFile}>" + } + result + }.minus(null).join('\n') + rncliCppModuleProviders = packages.collect { + it.libraryName ? """ auto module_${it.libraryName} = ${it.libraryName}_ModuleProvider(moduleName, params); + if (module_${it.libraryName} != nullptr) { + return module_${it.libraryName}; + }""" : null + }.minus(null).join("\n") + rncliCppComponentDescriptors = packages.collect { + def result = "" + if (it.componentDescriptors && it.componentDescriptors.size() > 0) { + result += it.componentDescriptors.collect { + " providerRegistry->add(concreteComponentDescriptorProvider<${it}>());" + }.join('\n') + } + result + }.join("\n") + } + + String generatedFileContents = generatedFileContentsTemplate + .replace("{{ rncliCppIncludes }}", rncliCppIncludes) + .replace("{{ rncliCppModuleProviders }}", rncliCppModuleProviders) + .replace("{{ rncliCppComponentDescriptors }}", rncliCppComponentDescriptors) + + outputDir.mkdirs() + final FileTreeBuilder treeBuilder = new FileTreeBuilder(outputDir) + treeBuilder.file(generatedFileName).newWriter().withWriter { w -> + w << generatedFileContents + } + } + + void generateRncliH(File outputDir, String generatedFileName, String generatedFileContentsTemplate) { + String generatedFileContents = generatedFileContentsTemplate + + outputDir.mkdirs() + final FileTreeBuilder treeBuilder = new FileTreeBuilder(outputDir) + treeBuilder.file(generatedFileName).newWriter().withWriter { w -> + w << generatedFileContents + } + } + /** * Runs a specified command using Runtime exec() in a specified directory. * Throws when the command result is empty. @@ -272,6 +418,10 @@ class ReactNativeModules { reactNativeModuleConfig.put("androidSourceDir", androidConfig["sourceDir"]) reactNativeModuleConfig.put("packageInstance", androidConfig["packageInstance"]) reactNativeModuleConfig.put("packageImportPath", androidConfig["packageImportPath"]) + reactNativeModuleConfig.put("libraryName", androidConfig["libraryName"]) + reactNativeModuleConfig.put("componentDescriptors", androidConfig["componentDescriptors"]) + reactNativeModuleConfig.put("androidMkPath", androidConfig["androidMkPath"]) + if (androidConfig["buildTypes"] && !androidConfig["buildTypes"].isEmpty()) { reactNativeModulesBuildVariants.put(nameCleansed, androidConfig["buildTypes"]) } @@ -326,6 +476,7 @@ ext.applyNativeModulesAppBuildGradle = { Project project, String root = null -> def generatedSrcDir = new File(buildDir, "generated/rncli/src/main/java") def generatedCodeDir = new File(generatedSrcDir, generatedFilePackage.replace('.', '/')) + def generatedJniDir = new File(buildDir, "generated/rncli/src/main/jni") task generatePackageList { doLast { @@ -333,8 +484,20 @@ ext.applyNativeModulesAppBuildGradle = { Project project, String root = null -> } } + task generateNewArchitectureFiles { + doLast { + autoModules.generateAndroidMkFile(generatedJniDir, "Android-rncli.mk", androidMkTemplate) + autoModules.generateRncliCpp(generatedJniDir, "rncli.cpp", rncliCppTemplate) + autoModules.generateRncliH(generatedJniDir, "rncli.h", rncliHTemplate) + } + } + preBuild.dependsOn generatePackageList + if (isNewArchitectureEnabled()) { + preBuild.dependsOn generateNewArchitectureFiles + } + android { sourceSets { main { @@ -345,3 +508,5 @@ ext.applyNativeModulesAppBuildGradle = { Project project, String root = null -> } } } + + diff --git a/packages/platform-android/src/config/__tests__/extractComponentDescriptors.test.ts b/packages/platform-android/src/config/__tests__/extractComponentDescriptors.test.ts new file mode 100644 index 000000000..9055d7839 --- /dev/null +++ b/packages/platform-android/src/config/__tests__/extractComponentDescriptors.test.ts @@ -0,0 +1,57 @@ +import {extractComponentDescriptors} from '../extractComponentDescriptors'; + +test('extracts TestComponentComponentDescriptor from basic fixture', () => { + const fixture = + "export default (codegenNativeComponent('TestComponent'): ComponentType);"; + expect(extractComponentDescriptors(fixture)).toEqual( + 'TestComponentComponentDescriptor', + ); +}); + +test('extracts TestComponentComponentDescriptor from when untyped', () => { + const fixture = "export default codegenNativeComponent('TestComponent')"; + expect(extractComponentDescriptors(fixture)).toEqual( + 'TestComponentComponentDescriptor', + ); +}); + +test('extracts TestComponentComponentDescriptor from when options are passed', () => { + const fixture = `export default (codegenNativeComponent('TestComponent', { + abc: d + }): ComponentType);`; + expect(extractComponentDescriptors(fixture)).toEqual( + 'TestComponentComponentDescriptor', + ); +}); + +test('extracts TestComponentComponentDescriptor from when options are passed 2', () => { + const fixture = + "export default (codegenNativeComponent('TestComponent', {abc: d}): ComponentType);"; + expect(extractComponentDescriptors(fixture)).toEqual( + 'TestComponentComponentDescriptor', + ); +}); + +test('extracts TestComponentComponentDescriptor from when options are passed empty', () => { + const fixture = + "export default (codegenNativeComponent('TestComponent', {}): ComponentType);"; + expect(extractComponentDescriptors(fixture)).toEqual( + 'TestComponentComponentDescriptor', + ); +}); + +test('skip when interfaceOnly is true', () => { + const fixture = `export default (codegenNativeComponent('TestComponent', { + interfaceOnly: true, + abc: d + }): ComponentType);`; + expect(extractComponentDescriptors(fixture)).toBeNull(); +}); + +test('skip when interfaceOnly is true 2', () => { + const fixture = `export default (codegenNativeComponent('TestComponent', { + abc: d, + interfaceOnly: true, + }): ComponentType);`; + expect(extractComponentDescriptors(fixture)).toBeNull(); +}); diff --git a/packages/platform-android/src/config/extractComponentDescriptors.ts b/packages/platform-android/src/config/extractComponentDescriptors.ts new file mode 100644 index 000000000..c3765a703 --- /dev/null +++ b/packages/platform-android/src/config/extractComponentDescriptors.ts @@ -0,0 +1,11 @@ +// TODO: avoid the regex and improve reliability by reading this data from codegen schema.json. +// Need to find a way to run "generateNewArchitectureFiles" gradle task after each library's "generateCodegenSchemaFromJavaScript" task. +const CODEGEN_NATIVE_COMPONENT_REGEX = /codegenNativeComponent(<.*>)?\s*\(\s*["'`](\w+)["'`](,?[\s\S]+interfaceOnly:\s*(\w+))?/m; + +export function extractComponentDescriptors(contents: string) { + const match = contents.match(CODEGEN_NATIVE_COMPONENT_REGEX); + if (!(match?.[4] === 'true') && match?.[2]) { + return `${match[2]}ComponentDescriptor`; + } + return null; +} diff --git a/packages/platform-android/src/config/findComponentDescriptors.ts b/packages/platform-android/src/config/findComponentDescriptors.ts new file mode 100644 index 000000000..8575e82ed --- /dev/null +++ b/packages/platform-android/src/config/findComponentDescriptors.ts @@ -0,0 +1,18 @@ +import fs from 'fs'; +import path from 'path'; +import glob from 'glob'; +import {extractComponentDescriptors} from './extractComponentDescriptors'; + +export function findComponentDescriptors(packageRoot: string) { + const files = glob.sync('**/+(*.js|*.jsx|*.ts|*.tsx)', {cwd: packageRoot}); + const codegenComponent = files + .map((filePath) => + fs.readFileSync(path.join(packageRoot, filePath), 'utf8'), + ) + .map(extractComponentDescriptors) + .filter(Boolean); + + // Filter out duplicates as it happens that libraries contain multiple outputs due to package publishing. + // TODO: consider using "codegenConfig" to avoid this. + return Array.from(new Set(codegenComponent as string[])); +} diff --git a/packages/platform-android/src/config/findLibraryName.ts b/packages/platform-android/src/config/findLibraryName.ts new file mode 100644 index 000000000..cfc760441 --- /dev/null +++ b/packages/platform-android/src/config/findLibraryName.ts @@ -0,0 +1,14 @@ +import fs from 'fs'; +import path from 'path'; + +export function findLibraryName(sourceDir: string) { + const buildGradlePath = path.join(sourceDir, 'build.gradle'); + if (fs.existsSync(buildGradlePath)) { + const buildGradleContents = fs.readFileSync(buildGradlePath, 'utf-8'); + const match = buildGradleContents.match(/libraryName = "(.+)"/); + if (match) { + return match[1]; + } + } + return undefined; +} diff --git a/packages/platform-android/src/config/index.ts b/packages/platform-android/src/config/index.ts index b4bf86ecb..c97916e8f 100644 --- a/packages/platform-android/src/config/index.ts +++ b/packages/platform-android/src/config/index.ts @@ -18,6 +18,8 @@ import { AndroidDependencyConfig, } from '@react-native-community/cli-types'; import {getPackageName} from './getAndroidProject'; +import {findLibraryName} from './findLibraryName'; +import {findComponentDescriptors} from './findComponentDescriptors'; /** * Gets android project config by analyzing given folder and taking some @@ -114,6 +116,12 @@ export function dependencyConfig( const buildTypes = userConfig.buildTypes || []; const dependencyConfiguration = userConfig.dependencyConfiguration; + const libraryName = userConfig.libraryName || findLibraryName(sourceDir); + const componentDescriptors = + userConfig.componentDescriptors || findComponentDescriptors(root); + const androidMkPath = userConfig.androidMkPath + ? path.join(sourceDir, userConfig.androidMkPath) + : path.join(sourceDir, 'build/generated/source/codegen/jni/Android.mk'); return { sourceDir, @@ -121,5 +129,8 @@ export function dependencyConfig( packageInstance, buildTypes, dependencyConfiguration, + libraryName, + componentDescriptors, + androidMkPath, }; }