From 9b5c405b2933c84daad561117a3eebacc65cbb7e Mon Sep 17 00:00:00 2001 From: Castro Agbo Date: Sat, 4 Jan 2025 21:52:30 +0000 Subject: [PATCH] feat(app-distribution): add Android app distribution plugin and configuration --- docs/app-distribution/usage/index.md | 2 +- packages/app-distribution/app.plugin.js | 1 + packages/app-distribution/package.json | 15 +- .../plugin/__tests__/README.md | 1 + .../__snapshots__/androidPlugin.test.ts.snap | 167 ++++++++++++++++++ .../plugin/__tests__/androidPlugin.test.ts | 32 ++++ .../__tests__/fixtures/app_build.gradle | 118 +++++++++++++ .../__tests__/fixtures/project_build.gradle | 38 ++++ .../plugin/src/android/applyPlugin.ts | 29 +++ .../src/android/buildscriptDependency.ts | 33 ++++ .../plugin/src/android/constants.ts | 7 + .../plugin/src/android/index.ts | 4 + packages/app-distribution/plugin/src/index.ts | 13 ++ .../app-distribution/plugin/tsconfig.json | 9 + packages/app/package.json | 3 +- yarn.lock | 6 + 16 files changed, 474 insertions(+), 4 deletions(-) create mode 100644 packages/app-distribution/app.plugin.js create mode 100644 packages/app-distribution/plugin/__tests__/README.md create mode 100644 packages/app-distribution/plugin/__tests__/__snapshots__/androidPlugin.test.ts.snap create mode 100644 packages/app-distribution/plugin/__tests__/androidPlugin.test.ts create mode 100644 packages/app-distribution/plugin/__tests__/fixtures/app_build.gradle create mode 100644 packages/app-distribution/plugin/__tests__/fixtures/project_build.gradle create mode 100644 packages/app-distribution/plugin/src/android/applyPlugin.ts create mode 100644 packages/app-distribution/plugin/src/android/buildscriptDependency.ts create mode 100644 packages/app-distribution/plugin/src/android/constants.ts create mode 100644 packages/app-distribution/plugin/src/android/index.ts create mode 100644 packages/app-distribution/plugin/src/index.ts create mode 100644 packages/app-distribution/plugin/tsconfig.json diff --git a/docs/app-distribution/usage/index.md b/docs/app-distribution/usage/index.md index 3b3ad8abef..29a8a3228c 100644 --- a/docs/app-distribution/usage/index.md +++ b/docs/app-distribution/usage/index.md @@ -24,7 +24,7 @@ cd ios/ && pod install ## Add the App Distribution Plugin -> This module does not handle Expo config plugins yet but does require a native integration similar to the perf module. If you want to add support for Expo to the App Distribution module we would welcome a PR! +> If you're using Expo, make sure to add the `@react-native-firebase/app-distribution` config plugin to your `app.json` or `app.config.js`. It handles the below installation steps for you. For instructions on how to do that, view the [Expo](/#expo) installation section. On Android, you need to install the Google App Distribution Plugin. diff --git a/packages/app-distribution/app.plugin.js b/packages/app-distribution/app.plugin.js new file mode 100644 index 0000000000..3c7d11b615 --- /dev/null +++ b/packages/app-distribution/app.plugin.js @@ -0,0 +1 @@ +module.exports = require('./plugin/build'); diff --git a/packages/app-distribution/package.json b/packages/app-distribution/package.json index f95cfa7972..9cd5edcd8c 100644 --- a/packages/app-distribution/package.json +++ b/packages/app-distribution/package.json @@ -8,7 +8,9 @@ "scripts": { "build": "genversion --semi lib/version.js", "build:clean": "rimraf android/build && rimraf ios/build", - "prepare": "yarn run build" + "build:plugin": "rimraf plugin/build && tsc --build plugin", + "lint:plugin": "eslint plugin/src/*", + "prepare": "yarn run build && yarn run build:plugin" }, "repository": { "type": "git", @@ -22,7 +24,16 @@ "app-distribution" ], "peerDependencies": { - "@react-native-firebase/app": "21.6.2" + "@react-native-firebase/app": "21.6.2", + "expo": ">=47.0.0" + }, + "devDependencies": { + "expo": "^50.0.21" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + } }, "publishConfig": { "access": "public" diff --git a/packages/app-distribution/plugin/__tests__/README.md b/packages/app-distribution/plugin/__tests__/README.md new file mode 100644 index 0000000000..d4b1e159de --- /dev/null +++ b/packages/app-distribution/plugin/__tests__/README.md @@ -0,0 +1 @@ +Please see the `packages/app/plugin/__tests__/README.md`. diff --git a/packages/app-distribution/plugin/__tests__/__snapshots__/androidPlugin.test.ts.snap b/packages/app-distribution/plugin/__tests__/__snapshots__/androidPlugin.test.ts.snap new file mode 100644 index 0000000000..00b8be9e6d --- /dev/null +++ b/packages/app-distribution/plugin/__tests__/__snapshots__/androidPlugin.test.ts.snap @@ -0,0 +1,167 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`App distribution Plugin Android Tests applies app distribution classpath to project build.gradle 1`] = ` +"// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + ext { + buildToolsVersion = "29.0.3" + minSdkVersion = 21 + compileSdkVersion = 30 + targetSdkVersion = 30 + } + repositories { + google() + jcenter() + } + dependencies { + classpath 'com.google.firebase:firebase-appdistribution-gradle:5.0.0' + classpath("com.android.tools.build:gradle:4.1.0") + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + mavenLocal() + maven { + // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm + url("$rootDir/../node_modules/react-native/android") + } + maven { + // Android JSC is installed from npm + url("$rootDir/../node_modules/jsc-android/dist") + } + + google() + jcenter() + maven { url 'https://www.jitpack.io' } + } +} +" +`; + +exports[`App distribution Plugin Android Tests applies app distribution plugin to app/build.gradle 1`] = ` +"/* Example build.gradle file from https://github.com/expo/expo/blob/6ab0274b5cb9a9c223e0d453787a522b438b4fcb/templates/expo-template-bare-minimum/android/app/build.gradle */ + +apply plugin: "com.android.application" + +import com.android.build.OutputFile + + +project.ext.react = [ + enableHermes: false +] + +apply from: '../../node_modules/react-native-unimodules/gradle.groovy' +apply from: "../../node_modules/react-native/react.gradle" +apply from: "../../node_modules/expo-constants/scripts/get-app-config-android.gradle" +apply from: "../../node_modules/expo-updates/scripts/create-manifest-android.gradle" + +def enableSeparateBuildPerCPUArchitecture = false + +def enableProguardInReleaseBuilds = false + +def jscFlavor = 'org.webkit:android-jsc:+' + +def enableHermes = project.ext.react.get("enableHermes", false); + +android { + compileSdkVersion rootProject.ext.compileSdkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + defaultConfig { + applicationId "com.helloworld" + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode 1 + versionName "1.0" + } + splits { + abi { + reset() + enable enableSeparateBuildPerCPUArchitecture + universalApk false // If true, also generate a universal APK + include "armeabi-v7a", "x86", "arm64-v8a", "x86_64" + } + } + signingConfigs { + debug { + storeFile file('debug.keystore') + storePassword 'android' + keyAlias 'androiddebugkey' + keyPassword 'android' + } + } + buildTypes { + debug { + signingConfig signingConfigs.debug + } + release { + // Caution! In production, you need to generate your own keystore file. + // see https://reactnative.dev/docs/signed-apk-android. + signingConfig signingConfigs.debug + minifyEnabled enableProguardInReleaseBuilds + proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" + } + } + + // applicationVariants are e.g. debug, release + applicationVariants.all { variant -> + variant.outputs.each { output -> + // For each separate APK per architecture, set a unique version code as described here: + // https://developer.android.com/studio/build/configure-apk-splits.html + def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4] + def abi = output.getFilter(OutputFile.ABI) + if (abi != null) { // null for the universal-debug, universal-release variants + output.versionCodeOverride = + versionCodes.get(abi) * 1048576 + defaultConfig.versionCode + } + + } + } +} + +dependencies { + implementation fileTree(dir: "libs", include: ["*.jar"]) + //noinspection GradleDynamicVersion + implementation "com.facebook.react:react-native:+" // From node_modules + implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0" + debugImplementation("com.facebook.flipper:flipper:\${FLIPPER_VERSION}") { + exclude group:'com.facebook.fbjni' + } + debugImplementation("com.facebook.flipper:flipper-network-plugin:\${FLIPPER_VERSION}") { + exclude group:'com.facebook.flipper' + exclude group:'com.squareup.okhttp3', module:'okhttp' + } + debugImplementation("com.facebook.flipper:flipper-fresco-plugin:\${FLIPPER_VERSION}") { + exclude group:'com.facebook.flipper' + } + addUnimodulesDependencies() + + if (enableHermes) { + def hermesPath = "../../node_modules/hermes-engine/android/"; + debugImplementation files(hermesPath + "hermes-debug.aar") + releaseImplementation files(hermesPath + "hermes-release.aar") + } else { + implementation jscFlavor + } +} + +// Run this once to be able to run the application with BUCK +// puts all compile dependencies into folder libs for BUCK to use +task copyDownloadableDepsToLibs(type: Copy) { + from configurations.compile + into 'libs' +} + +apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project) + +apply plugin: 'com.google.firebase.appdistribution'" +`; diff --git a/packages/app-distribution/plugin/__tests__/androidPlugin.test.ts b/packages/app-distribution/plugin/__tests__/androidPlugin.test.ts new file mode 100644 index 0000000000..47acc6555f --- /dev/null +++ b/packages/app-distribution/plugin/__tests__/androidPlugin.test.ts @@ -0,0 +1,32 @@ +import fs from 'fs/promises'; +import path from 'path'; +import { beforeAll, describe, expect, it } from '@jest/globals'; + +import { applyPlugin } from '../src/android/applyPlugin'; +import { setBuildscriptDependency } from '../src/android/buildscriptDependency'; + +describe('App distribution Plugin Android Tests', function () { + let appBuildGradle: string; + let projectBuildGradle: string; + + beforeAll(async function () { + projectBuildGradle = await fs.readFile( + path.resolve(__dirname, './fixtures/project_build.gradle'), + { encoding: 'utf-8' }, + ); + + appBuildGradle = await fs.readFile(path.resolve(__dirname, './fixtures/app_build.gradle'), { + encoding: 'utf-8', + }); + }); + + it('applies app distribution classpath to project build.gradle', async function () { + const result = setBuildscriptDependency(projectBuildGradle); + expect(result).toMatchSnapshot(); + }); + + it('applies app distribution plugin to app/build.gradle', async function () { + const result = applyPlugin(appBuildGradle); + expect(result).toMatchSnapshot(); + }); +}); diff --git a/packages/app-distribution/plugin/__tests__/fixtures/app_build.gradle b/packages/app-distribution/plugin/__tests__/fixtures/app_build.gradle new file mode 100644 index 0000000000..ce90569917 --- /dev/null +++ b/packages/app-distribution/plugin/__tests__/fixtures/app_build.gradle @@ -0,0 +1,118 @@ +/* Example build.gradle file from https://github.com/expo/expo/blob/6ab0274b5cb9a9c223e0d453787a522b438b4fcb/templates/expo-template-bare-minimum/android/app/build.gradle */ + +apply plugin: "com.android.application" + +import com.android.build.OutputFile + + +project.ext.react = [ + enableHermes: false +] + +apply from: '../../node_modules/react-native-unimodules/gradle.groovy' +apply from: "../../node_modules/react-native/react.gradle" +apply from: "../../node_modules/expo-constants/scripts/get-app-config-android.gradle" +apply from: "../../node_modules/expo-updates/scripts/create-manifest-android.gradle" + +def enableSeparateBuildPerCPUArchitecture = false + +def enableProguardInReleaseBuilds = false + +def jscFlavor = 'org.webkit:android-jsc:+' + +def enableHermes = project.ext.react.get("enableHermes", false); + +android { + compileSdkVersion rootProject.ext.compileSdkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + defaultConfig { + applicationId "com.helloworld" + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode 1 + versionName "1.0" + } + splits { + abi { + reset() + enable enableSeparateBuildPerCPUArchitecture + universalApk false // If true, also generate a universal APK + include "armeabi-v7a", "x86", "arm64-v8a", "x86_64" + } + } + signingConfigs { + debug { + storeFile file('debug.keystore') + storePassword 'android' + keyAlias 'androiddebugkey' + keyPassword 'android' + } + } + buildTypes { + debug { + signingConfig signingConfigs.debug + } + release { + // Caution! In production, you need to generate your own keystore file. + // see https://reactnative.dev/docs/signed-apk-android. + signingConfig signingConfigs.debug + minifyEnabled enableProguardInReleaseBuilds + proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" + } + } + + // applicationVariants are e.g. debug, release + applicationVariants.all { variant -> + variant.outputs.each { output -> + // For each separate APK per architecture, set a unique version code as described here: + // https://developer.android.com/studio/build/configure-apk-splits.html + def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4] + def abi = output.getFilter(OutputFile.ABI) + if (abi != null) { // null for the universal-debug, universal-release variants + output.versionCodeOverride = + versionCodes.get(abi) * 1048576 + defaultConfig.versionCode + } + + } + } +} + +dependencies { + implementation fileTree(dir: "libs", include: ["*.jar"]) + //noinspection GradleDynamicVersion + implementation "com.facebook.react:react-native:+" // From node_modules + implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0" + debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") { + exclude group:'com.facebook.fbjni' + } + debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") { + exclude group:'com.facebook.flipper' + exclude group:'com.squareup.okhttp3', module:'okhttp' + } + debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}") { + exclude group:'com.facebook.flipper' + } + addUnimodulesDependencies() + + if (enableHermes) { + def hermesPath = "../../node_modules/hermes-engine/android/"; + debugImplementation files(hermesPath + "hermes-debug.aar") + releaseImplementation files(hermesPath + "hermes-release.aar") + } else { + implementation jscFlavor + } +} + +// Run this once to be able to run the application with BUCK +// puts all compile dependencies into folder libs for BUCK to use +task copyDownloadableDepsToLibs(type: Copy) { + from configurations.compile + into 'libs' +} + +apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project) diff --git a/packages/app-distribution/plugin/__tests__/fixtures/project_build.gradle b/packages/app-distribution/plugin/__tests__/fixtures/project_build.gradle new file mode 100644 index 0000000000..4ff87f3b39 --- /dev/null +++ b/packages/app-distribution/plugin/__tests__/fixtures/project_build.gradle @@ -0,0 +1,38 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + ext { + buildToolsVersion = "29.0.3" + minSdkVersion = 21 + compileSdkVersion = 30 + targetSdkVersion = 30 + } + repositories { + google() + jcenter() + } + dependencies { + classpath("com.android.tools.build:gradle:4.1.0") + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + mavenLocal() + maven { + // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm + url("$rootDir/../node_modules/react-native/android") + } + maven { + // Android JSC is installed from npm + url("$rootDir/../node_modules/jsc-android/dist") + } + + google() + jcenter() + maven { url 'https://www.jitpack.io' } + } +} diff --git a/packages/app-distribution/plugin/src/android/applyPlugin.ts b/packages/app-distribution/plugin/src/android/applyPlugin.ts new file mode 100644 index 0000000000..9b1b630635 --- /dev/null +++ b/packages/app-distribution/plugin/src/android/applyPlugin.ts @@ -0,0 +1,29 @@ +import { ConfigPlugin, WarningAggregator, withAppBuildGradle } from '@expo/config-plugins'; +import { appDistributionMonitoringPlugin } from './constants'; + +/** + * Update `app/build.gradle` by applying app-distribution monitoring plugin + */ +export const withApplyappDistributionPlugin: ConfigPlugin = config => { + return withAppBuildGradle(config, config => { + if (config.modResults.language === 'groovy') { + config.modResults.contents = applyPlugin(config.modResults.contents); + } else { + WarningAggregator.addWarningAndroid( + 'react-native-firebase-app-distribution', + `Cannot automatically configure app build.gradle if it's not groovy`, + ); + } + return config; + }); +}; + +export function applyPlugin(appBuildGradle: string) { + const appDistributionPattern = new RegExp( + `apply\\s+plugin:\\s+['"]${appDistributionMonitoringPlugin}['"]`, + ); + if (!appBuildGradle.match(appDistributionPattern)) { + appBuildGradle += `\napply plugin: '${appDistributionMonitoringPlugin}'`; + } + return appBuildGradle; +} diff --git a/packages/app-distribution/plugin/src/android/buildscriptDependency.ts b/packages/app-distribution/plugin/src/android/buildscriptDependency.ts new file mode 100644 index 0000000000..9e09199c25 --- /dev/null +++ b/packages/app-distribution/plugin/src/android/buildscriptDependency.ts @@ -0,0 +1,33 @@ +import { ConfigPlugin, WarningAggregator, withProjectBuildGradle } from '@expo/config-plugins'; + +import { appDistributionMonitoringClassPath, appDistributionMonitoringVersion } from './constants'; + +/** + * Update `/build.gradle` by adding app-distribution dependency to buildscript + */ +export const withBuildscriptDependency: ConfigPlugin = config => { + return withProjectBuildGradle(config, config => { + if (config.modResults.language === 'groovy') { + config.modResults.contents = setBuildscriptDependency(config.modResults.contents); + } else { + WarningAggregator.addWarningAndroid( + 'react-native-firebase-app-distribution', + `Cannot automatically configure project build.gradle if it's not groovy`, + ); + } + return config; + }); +}; + +export function setBuildscriptDependency(buildGradle: string) { + // TODO: Find a more stable solution for this + if (!buildGradle.includes(appDistributionMonitoringClassPath)) { + return buildGradle.replace( + /dependencies\s?{/, + `dependencies { + classpath '${appDistributionMonitoringClassPath}:${appDistributionMonitoringVersion}'`, + ); + } + + return buildGradle; +} diff --git a/packages/app-distribution/plugin/src/android/constants.ts b/packages/app-distribution/plugin/src/android/constants.ts new file mode 100644 index 0000000000..0b1a9496b4 --- /dev/null +++ b/packages/app-distribution/plugin/src/android/constants.ts @@ -0,0 +1,7 @@ +const appPackageJson = require('@react-native-firebase/app/package.json'); + +export const appDistributionMonitoringClassPath = + 'com.google.firebase:firebase-appdistribution-gradle'; +export const appDistributionMonitoringPlugin = 'com.google.firebase.appdistribution'; +export const appDistributionMonitoringVersion = + appPackageJson.sdkVersions.android.firebaseAppDistributionGradle; diff --git a/packages/app-distribution/plugin/src/android/index.ts b/packages/app-distribution/plugin/src/android/index.ts new file mode 100644 index 0000000000..45185f5f2c --- /dev/null +++ b/packages/app-distribution/plugin/src/android/index.ts @@ -0,0 +1,4 @@ +import { withApplyappDistributionPlugin } from './applyPlugin'; +import { withBuildscriptDependency } from './buildscriptDependency'; + +export { withBuildscriptDependency, withApplyappDistributionPlugin }; diff --git a/packages/app-distribution/plugin/src/index.ts b/packages/app-distribution/plugin/src/index.ts new file mode 100644 index 0000000000..4b61eb3557 --- /dev/null +++ b/packages/app-distribution/plugin/src/index.ts @@ -0,0 +1,13 @@ +import { ConfigPlugin, withPlugins, createRunOncePlugin } from '@expo/config-plugins'; + +import { withApplyappDistributionPlugin, withBuildscriptDependency } from './android'; + +/** + * A config plugin for configuring `@react-native-firebase/app-distribution` + */ +const withRnFirebaseAppDistribution: ConfigPlugin = config => { + return withPlugins(config, [withBuildscriptDependency, withApplyappDistributionPlugin]); +}; + +const pak = require('@react-native-firebase/app-distribution/package.json'); +export default createRunOncePlugin(withRnFirebaseAppDistribution, pak.name, pak.version); diff --git a/packages/app-distribution/plugin/tsconfig.json b/packages/app-distribution/plugin/tsconfig.json new file mode 100644 index 0000000000..c2e8788648 --- /dev/null +++ b/packages/app-distribution/plugin/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@tsconfig/node-lts/tsconfig", + "compilerOptions": { + "outDir": "build", + "rootDir": "src", + "declaration": true + }, + "include": ["./src"] +} diff --git a/packages/app/package.json b/packages/app/package.json index edcaa827b2..16c84e5615 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -86,7 +86,8 @@ "firebaseCrashlyticsGradle": "3.0.2", "firebasePerfGradle": "1.4.2", "gmsGoogleServicesGradle": "4.4.2", - "playServicesAuth": "21.2.0" + "playServicesAuth": "21.2.0", + "firebaseAppDistributionGradle": "5.0.0" } } } diff --git a/yarn.lock b/yarn.lock index 1b1b113ee2..4afe1d5349 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6219,8 +6219,14 @@ __metadata: "@react-native-firebase/app-distribution@npm:21.6.2, @react-native-firebase/app-distribution@workspace:packages/app-distribution": version: 0.0.0-use.local resolution: "@react-native-firebase/app-distribution@workspace:packages/app-distribution" + dependencies: + expo: "npm:^50.0.21" peerDependencies: "@react-native-firebase/app": 21.6.2 + expo: ">=47.0.0" + peerDependenciesMeta: + expo: + optional: true languageName: unknown linkType: soft