From 832057cfbdf1778ad2141a1ad4466d2e8c24b8ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Klocek?= Date: Thu, 22 Jul 2021 18:04:06 +0200 Subject: [PATCH] feat: Add Expo config plugin (#5480) * Preparations & Added the `app` plugin * Add perf monitoring plugin * Add crashlytics plugin * eslint ignore `plugin/build` dirs * Add tests READMEs --- .eslintignore | 1 + .prettierignore | 1 + package.json | 1 + packages/app/app.plugin.js | 1 + packages/app/package.json | 8 +- packages/app/plugin/__tests__/README.md | 19 ++ .../__snapshots__/androidPlugin.test.ts.snap | 167 ++++++++++++++++++ .../__snapshots__/iosPlugin.test.ts.snap | 106 +++++++++++ .../plugin/__tests__/androidPlugin.test.ts | 31 ++++ .../plugin/__tests__/fixtures/AppDelegate.m | 99 +++++++++++ .../__tests__/fixtures/app_build.gradle | 118 +++++++++++++ .../__tests__/fixtures/project_build.gradle | 38 ++++ .../app/plugin/__tests__/iosPlugin.test.ts | 14 ++ .../app/plugin/src/android/applyPlugin.ts | 30 ++++ .../src/android/buildscriptDependency.ts | 33 ++++ packages/app/plugin/src/android/constants.ts | 7 + .../plugin/src/android/copyGoogleServices.ts | 36 ++++ packages/app/plugin/src/android/index.ts | 5 + packages/app/plugin/src/index.ts | 27 +++ packages/app/plugin/src/ios/appDelegate.ts | 48 +++++ .../app/plugin/src/ios/googleServicesPlist.ts | 55 ++++++ packages/app/plugin/src/ios/index.ts | 4 + packages/app/plugin/tsconfig.json | 9 + packages/crashlytics/app.plugin.js | 1 + packages/crashlytics/package.json | 5 +- .../crashlytics/plugin/__tests__/README.md | 1 + .../__snapshots__/androidPlugin.test.ts.snap | 167 ++++++++++++++++++ .../plugin/__tests__/androidPlugin.test.ts | 31 ++++ .../__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 | 5 + .../crashlytics/plugin/src/android/index.ts | 4 + packages/crashlytics/plugin/src/index.ts | 13 ++ packages/crashlytics/plugin/tsconfig.json | 9 + packages/perf/app.plugin.js | 1 + packages/perf/package.json | 7 +- packages/perf/plugin/__tests__/README.md | 1 + .../__snapshots__/androidPlugin.test.ts.snap | 167 ++++++++++++++++++ .../plugin/__tests__/androidPlugin.test.ts | 31 ++++ .../__tests__/fixtures/app_build.gradle | 118 +++++++++++++ .../__tests__/fixtures/project_build.gradle | 38 ++++ .../perf/plugin/src/android/applyPlugin.ts | 27 +++ .../src/android/buildscriptDependency.ts | 33 ++++ packages/perf/plugin/src/android/constants.ts | 5 + packages/perf/plugin/src/android/index.ts | 4 + packages/perf/plugin/src/index.ts | 13 ++ packages/perf/plugin/tsconfig.json | 9 + tsconfig.json | 1 + yarn.lock | 129 +++++++++++++- 51 files changed, 1888 insertions(+), 8 deletions(-) create mode 100644 packages/app/app.plugin.js create mode 100644 packages/app/plugin/__tests__/README.md create mode 100644 packages/app/plugin/__tests__/__snapshots__/androidPlugin.test.ts.snap create mode 100644 packages/app/plugin/__tests__/__snapshots__/iosPlugin.test.ts.snap create mode 100644 packages/app/plugin/__tests__/androidPlugin.test.ts create mode 100644 packages/app/plugin/__tests__/fixtures/AppDelegate.m create mode 100644 packages/app/plugin/__tests__/fixtures/app_build.gradle create mode 100644 packages/app/plugin/__tests__/fixtures/project_build.gradle create mode 100644 packages/app/plugin/__tests__/iosPlugin.test.ts create mode 100644 packages/app/plugin/src/android/applyPlugin.ts create mode 100644 packages/app/plugin/src/android/buildscriptDependency.ts create mode 100644 packages/app/plugin/src/android/constants.ts create mode 100644 packages/app/plugin/src/android/copyGoogleServices.ts create mode 100644 packages/app/plugin/src/android/index.ts create mode 100644 packages/app/plugin/src/index.ts create mode 100644 packages/app/plugin/src/ios/appDelegate.ts create mode 100644 packages/app/plugin/src/ios/googleServicesPlist.ts create mode 100644 packages/app/plugin/src/ios/index.ts create mode 100644 packages/app/plugin/tsconfig.json create mode 100644 packages/crashlytics/app.plugin.js create mode 100644 packages/crashlytics/plugin/__tests__/README.md create mode 100644 packages/crashlytics/plugin/__tests__/__snapshots__/androidPlugin.test.ts.snap create mode 100644 packages/crashlytics/plugin/__tests__/androidPlugin.test.ts create mode 100644 packages/crashlytics/plugin/__tests__/fixtures/app_build.gradle create mode 100644 packages/crashlytics/plugin/__tests__/fixtures/project_build.gradle create mode 100644 packages/crashlytics/plugin/src/android/applyPlugin.ts create mode 100644 packages/crashlytics/plugin/src/android/buildscriptDependency.ts create mode 100644 packages/crashlytics/plugin/src/android/constants.ts create mode 100644 packages/crashlytics/plugin/src/android/index.ts create mode 100644 packages/crashlytics/plugin/src/index.ts create mode 100644 packages/crashlytics/plugin/tsconfig.json create mode 100644 packages/perf/app.plugin.js create mode 100644 packages/perf/plugin/__tests__/README.md create mode 100644 packages/perf/plugin/__tests__/__snapshots__/androidPlugin.test.ts.snap create mode 100644 packages/perf/plugin/__tests__/androidPlugin.test.ts create mode 100644 packages/perf/plugin/__tests__/fixtures/app_build.gradle create mode 100644 packages/perf/plugin/__tests__/fixtures/project_build.gradle create mode 100644 packages/perf/plugin/src/android/applyPlugin.ts create mode 100644 packages/perf/plugin/src/android/buildscriptDependency.ts create mode 100644 packages/perf/plugin/src/android/constants.ts create mode 100644 packages/perf/plugin/src/android/index.ts create mode 100644 packages/perf/plugin/src/index.ts create mode 100644 packages/perf/plugin/tsconfig.json diff --git a/.eslintignore b/.eslintignore index 40fdd87fa1..f906059ad2 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,5 +1,6 @@ src/version.js packages/**/node_modules/** +packages/**/plugin/build/** node_modules scripts/ coverage diff --git a/.prettierignore b/.prettierignore index 1896d186c0..87f09a844a 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,5 +1,6 @@ package.json packages/**/node_modules/** +packages/**/plugin/build/** tests/node_modules/** tests/app.playground.js tests/app.smartreply.js diff --git a/package.json b/package.json index 0fa38d2e6c..d58111905c 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "@babel/core": "^7.14.0", "@babel/preset-env": "7.14.1", "@octokit/core": "^3.3.1", + "@tsconfig/node12": "^1.0.9", "@types/jest": "^26.0.23", "@types/react": "^17.0.5", "@types/react-native": "^0.64.4", diff --git a/packages/app/app.plugin.js b/packages/app/app.plugin.js new file mode 100644 index 0000000000..3c7d11b615 --- /dev/null +++ b/packages/app/app.plugin.js @@ -0,0 +1 @@ +module.exports = require('./plugin/build'); diff --git a/packages/app/package.json b/packages/app/package.json index e135cec9e8..1ca8184263 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -9,7 +9,9 @@ "build": "genversion --semi lib/version.js && npm run build:version", "build:version": "node ./scripts/genversion-ios && node ./scripts/genversion-android", "build:clean": "rimraf android/build && rimraf ios/build", - "prepare": "npm run build" + "build:plugin": "rimraf plugin/build && tsc --build plugin", + "lint:plugin": "eslint plugin/src/*", + "prepare": "npm run build && npm run build:plugin" }, "repository": { "type": "git", @@ -54,6 +56,7 @@ "react-native": "*" }, "dependencies": { + "@expo/config-plugins": "^3.0.2", "opencollective-postinstall": "^2.0.1", "superstruct": "^0.6.2" }, @@ -70,6 +73,9 @@ "compileSdk": 30, "buildTools": "30.0.2", "firebase": "28.2.1", + "firebaseCrashlyticsGradle": "2.7.1", + "firebasePerfGradle": "1.4.0", + "gmsGoogleServicesGradle": "4.3.8", "playServicesAuth": "19.0.0" } } diff --git a/packages/app/plugin/__tests__/README.md b/packages/app/plugin/__tests__/README.md new file mode 100644 index 0000000000..13b9757295 --- /dev/null +++ b/packages/app/plugin/__tests__/README.md @@ -0,0 +1,19 @@ +## Expo Config Plugin unit tests + +To test the changes to native code applied by config plugins, [snapshot tests](https://jestjs.io/docs/snapshot-testing) are used. Plugin test flow, in short: + +1. A test fixture is loaded. In this case, fixtures are template files (`build.gradle`, `AppDelegate.m` etc.) from [`expo-template-bare-minimum`](https://github.com/expo/expo/tree/master/templates/expo-template-bare-minimum). +2. Plugin changes are applied (e.g. gradle dependency is added). +3. Modified file is compared with previously saved snapshot. If they're equal, the test passes. If not, the test fails and the difference (actual vs expected) is shown. + +You can preview the snapshot files manually, by opening `__snapshots__/*.snap` files. + +### Updating the snapshots + +Snapshot tests are designed to ensure the plugin result will not change. In case you intentionally modified the plugin behavior (e.g. updated gradle dependency versions), you have to update the snapshots, otherwise the tests will fail. There are two ways to do it: + +- Update all snapshots by running `npm run tests:jest -u`. +- Update snapshots interactively, one by one: + 1. Run `yarn tests:jest --watchAll` + 2. Press `i` to let `jest` display changes and prompt you for updating each snapshot. + > This option is not available, when there are no failing snapshots diff --git a/packages/app/plugin/__tests__/__snapshots__/androidPlugin.test.ts.snap b/packages/app/plugin/__tests__/__snapshots__/androidPlugin.test.ts.snap new file mode 100644 index 0000000000..f2343ae202 --- /dev/null +++ b/packages/app/plugin/__tests__/__snapshots__/androidPlugin.test.ts.snap @@ -0,0 +1,167 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Config Plugin Android Tests applies changes 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.gms.google-services'" +`; + +exports[`Config Plugin Android Tests applies changes 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.gms:google-services:4.3.8' + 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/plugin/__tests__/__snapshots__/iosPlugin.test.ts.snap b/packages/app/plugin/__tests__/__snapshots__/iosPlugin.test.ts.snap new file mode 100644 index 0000000000..f815ca7187 --- /dev/null +++ b/packages/app/plugin/__tests__/__snapshots__/iosPlugin.test.ts.snap @@ -0,0 +1,106 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Config Plugin iOS Tests tests changes made to AppDelegate.m 1`] = ` +"#import \\"AppDelegate.h\\" +@import Firebase; + +#import +#import +#import +#import + +#import +#import +#import +#import +#import + +#if defined(FB_SONARKIT_ENABLED) && __has_include() +#import +#import +#import +#import +#import +#import + +static void InitializeFlipper(UIApplication *application) { + FlipperClient *client = [FlipperClient sharedClient]; + SKDescriptorMapper *layoutDescriptorMapper = [[SKDescriptorMapper alloc] initWithDefaults]; + [client addPlugin:[[FlipperKitLayoutPlugin alloc] initWithRootNode:application withDescriptorMapper:layoutDescriptorMapper]]; + [client addPlugin:[[FKUserDefaultsPlugin alloc] initWithSuiteName:nil]]; + [client addPlugin:[FlipperKitReactPlugin new]]; + [client addPlugin:[[FlipperKitNetworkPlugin alloc] initWithNetworkAdapter:[SKIOSNetworkAdapter new]]]; + [client start]; +} +#endif + +@interface AppDelegate () + +@property (nonatomic, strong) UMModuleRegistryAdapter *moduleRegistryAdapter; +@property (nonatomic, strong) NSDictionary *launchOptions; + +@end + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ +#if defined(FB_SONARKIT_ENABLED) && __has_include() + InitializeFlipper(application); +#endif + + [FIRApp configure]; + self.moduleRegistryAdapter = [[UMModuleRegistryAdapter alloc] initWithModuleRegistryProvider:[[UMModuleRegistryProvider alloc] init]]; + self.launchOptions = launchOptions; + self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + #ifdef DEBUG + [self initializeReactNativeApp]; + #else + EXUpdatesAppController *controller = [EXUpdatesAppController sharedInstance]; + controller.delegate = self; + [controller startAndShowLaunchScreen:self.window]; + #endif + + [super application:application didFinishLaunchingWithOptions:launchOptions]; + + return YES; +} + +- (RCTBridge *)initializeReactNativeApp +{ + RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:self.launchOptions]; + RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:@\\"main\\" initialProperties:nil]; + rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1]; + + UIViewController *rootViewController = [UIViewController new]; + rootViewController.view = rootView; + self.window.rootViewController = rootViewController; + [self.window makeKeyAndVisible]; + + return bridge; + } + +- (NSArray> *)extraModulesForBridge:(RCTBridge *)bridge +{ + NSArray> *extraModules = [_moduleRegistryAdapter extraModulesForBridge:bridge]; + // If you'd like to export some custom RCTBridgeModules that are not Expo modules, add them here! + return extraModules; +} + +- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge { + #ifdef DEBUG + return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@\\"index\\" fallbackResource:nil]; + #else + return [[EXUpdatesAppController sharedInstance] launchAssetUrl]; + #endif +} + +- (void)appController:(EXUpdatesAppController *)appController didStartWithSuccess:(BOOL)success { + appController.bridge = [self initializeReactNativeApp]; + EXSplashScreenService *splashScreenService = (EXSplashScreenService *)[UMModuleRegistryProvider getSingletonModuleForClass:[EXSplashScreenService class]]; + [splashScreenService showSplashScreenFor:self.window.rootViewController]; +} + +@end +" +`; diff --git a/packages/app/plugin/__tests__/androidPlugin.test.ts b/packages/app/plugin/__tests__/androidPlugin.test.ts new file mode 100644 index 0000000000..91e6608056 --- /dev/null +++ b/packages/app/plugin/__tests__/androidPlugin.test.ts @@ -0,0 +1,31 @@ +import fs from 'fs/promises'; +import path from 'path'; + +import { applyPlugin } from '../src/android/applyPlugin'; +import { setBuildscriptDependency } from '../src/android/buildscriptDependency'; + +describe('Config 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 changes to project build.gradle', async function () { + const result = setBuildscriptDependency(projectBuildGradle); + expect(result).toMatchSnapshot(); + }); + + it('applies changes to app/build.gradle', async function () { + const result = applyPlugin(appBuildGradle); + expect(result).toMatchSnapshot(); + }); +}); diff --git a/packages/app/plugin/__tests__/fixtures/AppDelegate.m b/packages/app/plugin/__tests__/fixtures/AppDelegate.m new file mode 100644 index 0000000000..755f2c0b47 --- /dev/null +++ b/packages/app/plugin/__tests__/fixtures/AppDelegate.m @@ -0,0 +1,99 @@ +#import "AppDelegate.h" + +#import +#import +#import +#import + +#import +#import +#import +#import +#import + +#if defined(FB_SONARKIT_ENABLED) && __has_include() +#import +#import +#import +#import +#import +#import + +static void InitializeFlipper(UIApplication *application) { + FlipperClient *client = [FlipperClient sharedClient]; + SKDescriptorMapper *layoutDescriptorMapper = [[SKDescriptorMapper alloc] initWithDefaults]; + [client addPlugin:[[FlipperKitLayoutPlugin alloc] initWithRootNode:application withDescriptorMapper:layoutDescriptorMapper]]; + [client addPlugin:[[FKUserDefaultsPlugin alloc] initWithSuiteName:nil]]; + [client addPlugin:[FlipperKitReactPlugin new]]; + [client addPlugin:[[FlipperKitNetworkPlugin alloc] initWithNetworkAdapter:[SKIOSNetworkAdapter new]]]; + [client start]; +} +#endif + +@interface AppDelegate () + +@property (nonatomic, strong) UMModuleRegistryAdapter *moduleRegistryAdapter; +@property (nonatomic, strong) NSDictionary *launchOptions; + +@end + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ +#if defined(FB_SONARKIT_ENABLED) && __has_include() + InitializeFlipper(application); +#endif + + self.moduleRegistryAdapter = [[UMModuleRegistryAdapter alloc] initWithModuleRegistryProvider:[[UMModuleRegistryProvider alloc] init]]; + self.launchOptions = launchOptions; + self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + #ifdef DEBUG + [self initializeReactNativeApp]; + #else + EXUpdatesAppController *controller = [EXUpdatesAppController sharedInstance]; + controller.delegate = self; + [controller startAndShowLaunchScreen:self.window]; + #endif + + [super application:application didFinishLaunchingWithOptions:launchOptions]; + + return YES; +} + +- (RCTBridge *)initializeReactNativeApp +{ + RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:self.launchOptions]; + RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:@"main" initialProperties:nil]; + rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1]; + + UIViewController *rootViewController = [UIViewController new]; + rootViewController.view = rootView; + self.window.rootViewController = rootViewController; + [self.window makeKeyAndVisible]; + + return bridge; + } + +- (NSArray> *)extraModulesForBridge:(RCTBridge *)bridge +{ + NSArray> *extraModules = [_moduleRegistryAdapter extraModulesForBridge:bridge]; + // If you'd like to export some custom RCTBridgeModules that are not Expo modules, add them here! + return extraModules; +} + +- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge { + #ifdef DEBUG + return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil]; + #else + return [[EXUpdatesAppController sharedInstance] launchAssetUrl]; + #endif +} + +- (void)appController:(EXUpdatesAppController *)appController didStartWithSuccess:(BOOL)success { + appController.bridge = [self initializeReactNativeApp]; + EXSplashScreenService *splashScreenService = (EXSplashScreenService *)[UMModuleRegistryProvider getSingletonModuleForClass:[EXSplashScreenService class]]; + [splashScreenService showSplashScreenFor:self.window.rootViewController]; +} + +@end diff --git a/packages/app/plugin/__tests__/fixtures/app_build.gradle b/packages/app/plugin/__tests__/fixtures/app_build.gradle new file mode 100644 index 0000000000..ce90569917 --- /dev/null +++ b/packages/app/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/plugin/__tests__/fixtures/project_build.gradle b/packages/app/plugin/__tests__/fixtures/project_build.gradle new file mode 100644 index 0000000000..4ff87f3b39 --- /dev/null +++ b/packages/app/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/plugin/__tests__/iosPlugin.test.ts b/packages/app/plugin/__tests__/iosPlugin.test.ts new file mode 100644 index 0000000000..18f403a3de --- /dev/null +++ b/packages/app/plugin/__tests__/iosPlugin.test.ts @@ -0,0 +1,14 @@ +import fs from 'fs/promises'; +import path from 'path'; + +import { modifyObjcAppDelegate } from '../src/ios/appDelegate'; + +describe('Config Plugin iOS Tests', function () { + it('tests changes made to AppDelegate.m', async function () { + const appDelegate = await fs.readFile(path.join(__dirname, './fixtures/AppDelegate.m'), { + encoding: 'utf8', + }); + const result = modifyObjcAppDelegate(appDelegate); + expect(result).toMatchSnapshot(); + }); +}); diff --git a/packages/app/plugin/src/android/applyPlugin.ts b/packages/app/plugin/src/android/applyPlugin.ts new file mode 100644 index 0000000000..8212901a0c --- /dev/null +++ b/packages/app/plugin/src/android/applyPlugin.ts @@ -0,0 +1,30 @@ +import { ConfigPlugin, WarningAggregator, withAppBuildGradle } from '@expo/config-plugins'; + +import { googleServicesPlugin } from './constants'; + +/** + * Update `app/build.gradle` by applying google-services plugin + */ +export const withApplyGoogleServicesPlugin: 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', + `Cannot automatically configure app build.gradle if it's not groovy`, + ); + } + return config; + }); +}; + +export function applyPlugin(appBuildGradle: string) { + // Make sure the project does not have the plugin already + const pattern = new RegExp(`apply\\s+plugin:\\s+['"]${googleServicesPlugin}['"]`); + if (!appBuildGradle.match(pattern)) { + return appBuildGradle + `\napply plugin: '${googleServicesPlugin}'`; + } + + return appBuildGradle; +} diff --git a/packages/app/plugin/src/android/buildscriptDependency.ts b/packages/app/plugin/src/android/buildscriptDependency.ts new file mode 100644 index 0000000000..787ce7e8d2 --- /dev/null +++ b/packages/app/plugin/src/android/buildscriptDependency.ts @@ -0,0 +1,33 @@ +import { ConfigPlugin, WarningAggregator, withProjectBuildGradle } from '@expo/config-plugins'; + +import { googleServicesClassPath, googleServicesVersion } from './constants'; + +/** + * Update `/build.gradle` by adding google-services 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', + `Cannot automatically configure project build.gradle if it's not groovy`, + ); + } + return config; + }); +}; + +export function setBuildscriptDependency(buildGradle: string) { + if (!buildGradle.includes(googleServicesClassPath)) { + // TODO: Find a more stable solution for this + return buildGradle.replace( + /dependencies\s?{/, + `dependencies { + classpath '${googleServicesClassPath}:${googleServicesVersion}'`, + ); + } else { + return buildGradle; + } +} diff --git a/packages/app/plugin/src/android/constants.ts b/packages/app/plugin/src/android/constants.ts new file mode 100644 index 0000000000..4694567fa7 --- /dev/null +++ b/packages/app/plugin/src/android/constants.ts @@ -0,0 +1,7 @@ +const appPackageJson = require('@react-native-firebase/app/package.json'); + +export const DEFAULT_TARGET_PATH = 'app/google-services.json'; + +export const googleServicesClassPath = 'com.google.gms:google-services'; +export const googleServicesPlugin = 'com.google.gms.google-services'; +export const googleServicesVersion = appPackageJson.sdkVersions.android.gmsGoogleServicesGradle; diff --git a/packages/app/plugin/src/android/copyGoogleServices.ts b/packages/app/plugin/src/android/copyGoogleServices.ts new file mode 100644 index 0000000000..47b4fc8f80 --- /dev/null +++ b/packages/app/plugin/src/android/copyGoogleServices.ts @@ -0,0 +1,36 @@ +import { ConfigPlugin, withDangerousMod } from '@expo/config-plugins'; + +import { DEFAULT_TARGET_PATH } from './constants'; +import path from 'path'; +import fs from 'fs/promises'; + +/** + * Copy `google-services.json` + */ +export const withCopyAndroidGoogleServices: ConfigPlugin = config => { + return withDangerousMod(config, [ + 'android', + async config => { + if (!config.android?.googleServicesFile) { + throw new Error( + 'Path to google-services.json is not defined. Please specify the `expo.android.googleServicesFile` field in app.json.', + ); + } + + const srcPath = path.resolve( + config.modRequest.projectRoot, + config.android.googleServicesFile, + ); + const destPath = path.resolve(config.modRequest.platformProjectRoot, DEFAULT_TARGET_PATH); + + try { + await fs.copyFile(srcPath, destPath); + } catch (e) { + throw new Error( + `Cannot copy google-services.json, because the file ${srcPath} doesn't exist. Please provide a valid path in \`app.json\`.`, + ); + } + return config; + }, + ]); +}; diff --git a/packages/app/plugin/src/android/index.ts b/packages/app/plugin/src/android/index.ts new file mode 100644 index 0000000000..5b8c87369d --- /dev/null +++ b/packages/app/plugin/src/android/index.ts @@ -0,0 +1,5 @@ +import { withApplyGoogleServicesPlugin } from './applyPlugin'; +import { withBuildscriptDependency } from './buildscriptDependency'; +import { withCopyAndroidGoogleServices } from './copyGoogleServices'; + +export { withBuildscriptDependency, withApplyGoogleServicesPlugin, withCopyAndroidGoogleServices }; diff --git a/packages/app/plugin/src/index.ts b/packages/app/plugin/src/index.ts new file mode 100644 index 0000000000..441ef3967a --- /dev/null +++ b/packages/app/plugin/src/index.ts @@ -0,0 +1,27 @@ +import { ConfigPlugin, withPlugins, createRunOncePlugin } from '@expo/config-plugins'; + +import { + withApplyGoogleServicesPlugin, + withBuildscriptDependency, + withCopyAndroidGoogleServices, +} from './android'; +import { withFirebaseAppDelegate, withIosGoogleServicesFile } from './ios'; + +/** + * A config plugin for configuring `@react-native-firebase/app` + */ +const withRnFirebaseApp: ConfigPlugin = config => { + return withPlugins(config, [ + // iOS + withFirebaseAppDelegate, + withIosGoogleServicesFile, + + // Android + withBuildscriptDependency, + withApplyGoogleServicesPlugin, + withCopyAndroidGoogleServices, + ]); +}; + +const pak = require('@react-native-firebase/app/package.json'); +export default createRunOncePlugin(withRnFirebaseApp, pak.name, pak.version); diff --git a/packages/app/plugin/src/ios/appDelegate.ts b/packages/app/plugin/src/ios/appDelegate.ts new file mode 100644 index 0000000000..fcb2642153 --- /dev/null +++ b/packages/app/plugin/src/ios/appDelegate.ts @@ -0,0 +1,48 @@ +import { ConfigPlugin, IOSConfig, withDangerousMod } from '@expo/config-plugins'; +import fs from 'fs/promises'; + +const methodInvocationBlock = `[FIRApp configure];`; + +export function modifyObjcAppDelegate(contents: string): string { + // Add import + if (!contents.includes('@import Firebase;')) { + contents = contents.replace( + /#import "AppDelegate.h"/g, + `#import "AppDelegate.h" +@import Firebase;`, + ); + } + + // Add invocation + if (!contents.includes(methodInvocationBlock)) { + // self.moduleRegistryAdapter = [[UMModuleRegistryAdapter alloc] + contents = contents.replace( + /self\.moduleRegistryAdapter = \[\[UMModuleRegistryAdapter alloc\]/g, + `${methodInvocationBlock} + self.moduleRegistryAdapter = [[UMModuleRegistryAdapter alloc]`, + ); + } + + return contents; +} + +export const withFirebaseAppDelegate: ConfigPlugin = config => { + return withDangerousMod(config, [ + 'ios', + async config => { + const fileInfo = IOSConfig.Paths.getAppDelegate(config.modRequest.projectRoot); + let contents = await fs.readFile(fileInfo.path, 'utf-8'); + if (fileInfo.language === 'objc') { + contents = modifyObjcAppDelegate(contents); + } else { + // TODO: Support Swift + throw new Error( + `Cannot add Firebase code to AppDelegate of language "${fileInfo.language}"`, + ); + } + await fs.writeFile(fileInfo.path, contents); + + return config; + }, + ]); +}; diff --git a/packages/app/plugin/src/ios/googleServicesPlist.ts b/packages/app/plugin/src/ios/googleServicesPlist.ts new file mode 100644 index 0000000000..1a6b14c224 --- /dev/null +++ b/packages/app/plugin/src/ios/googleServicesPlist.ts @@ -0,0 +1,55 @@ +import { ConfigPlugin, IOSConfig, withXcodeProject, XcodeProject } from '@expo/config-plugins'; +import fs from 'fs'; +import path from 'path'; + +export const withIosGoogleServicesFile: ConfigPlugin = config => { + return withXcodeProject(config, config => { + if (!config.ios?.googleServicesFile) { + throw new Error( + 'Path to GoogleService-Info.plist is not defined. Please specify the `expo.ios.googleServicesFile` field in app.json.', + ); + } + + config.modResults = setGoogleServicesFile({ + projectRoot: config.modRequest.projectRoot, + project: config.modResults, + googleServicesFileRelativePath: config.ios.googleServicesFile, + }); + return config; + }); +}; + +export function setGoogleServicesFile({ + projectRoot, + project, + googleServicesFileRelativePath, +}: { + project: XcodeProject; + projectRoot: string; + googleServicesFileRelativePath: string; +}): XcodeProject { + const googleServiceFilePath = path.resolve(projectRoot, googleServicesFileRelativePath); + + if (!fs.existsSync(googleServiceFilePath)) { + throw new Error( + `GoogleService-Info.plist doesn't exist in ${googleServiceFilePath}. Place it there or configure the path in app.json`, + ); + } + + fs.copyFileSync( + googleServiceFilePath, + path.join(IOSConfig.Paths.getSourceRoot(projectRoot), 'GoogleService-Info.plist'), + ); + + const projectName = IOSConfig.XcodeUtils.getProjectName(projectRoot); + const plistFilePath = `${projectName}/GoogleService-Info.plist`; + if (!project.hasFile(plistFilePath)) { + project = IOSConfig.XcodeUtils.addResourceFileToGroup({ + filepath: plistFilePath, + groupName: projectName, + project, + isBuildFile: true, + }); + } + return project; +} diff --git a/packages/app/plugin/src/ios/index.ts b/packages/app/plugin/src/ios/index.ts new file mode 100644 index 0000000000..912045e913 --- /dev/null +++ b/packages/app/plugin/src/ios/index.ts @@ -0,0 +1,4 @@ +import { withFirebaseAppDelegate } from './appDelegate'; +import { withIosGoogleServicesFile } from './googleServicesPlist'; + +export { withIosGoogleServicesFile, withFirebaseAppDelegate }; diff --git a/packages/app/plugin/tsconfig.json b/packages/app/plugin/tsconfig.json new file mode 100644 index 0000000000..a68b89f126 --- /dev/null +++ b/packages/app/plugin/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@tsconfig/node12/tsconfig.json", + "compilerOptions": { + "outDir": "build", + "rootDir": "src", + "declaration": true + }, + "include": ["./src"] +} diff --git a/packages/crashlytics/app.plugin.js b/packages/crashlytics/app.plugin.js new file mode 100644 index 0000000000..3c7d11b615 --- /dev/null +++ b/packages/crashlytics/app.plugin.js @@ -0,0 +1 @@ +module.exports = require('./plugin/build'); diff --git a/packages/crashlytics/package.json b/packages/crashlytics/package.json index a0b0b5ad09..bfc405a8c6 100644 --- a/packages/crashlytics/package.json +++ b/packages/crashlytics/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", @@ -30,6 +32,7 @@ "@react-native-firebase/app": "12.3.0" }, "dependencies": { + "@expo/config-plugins": "^3.0.2", "stacktrace-js": "^2.0.0" }, "publishConfig": { diff --git a/packages/crashlytics/plugin/__tests__/README.md b/packages/crashlytics/plugin/__tests__/README.md new file mode 100644 index 0000000000..d4b1e159de --- /dev/null +++ b/packages/crashlytics/plugin/__tests__/README.md @@ -0,0 +1 @@ +Please see the `packages/app/plugin/__tests__/README.md`. diff --git a/packages/crashlytics/plugin/__tests__/__snapshots__/androidPlugin.test.ts.snap b/packages/crashlytics/plugin/__tests__/__snapshots__/androidPlugin.test.ts.snap new file mode 100644 index 0000000000..88caa90b52 --- /dev/null +++ b/packages/crashlytics/plugin/__tests__/__snapshots__/androidPlugin.test.ts.snap @@ -0,0 +1,167 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Crashlytics Plugin Android Tests applies crashlytics 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-crashlytics-gradle:2.7.1' + 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[`Crashlytics Plugin Android Tests applies crashlytics 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.crashlytics'" +`; diff --git a/packages/crashlytics/plugin/__tests__/androidPlugin.test.ts b/packages/crashlytics/plugin/__tests__/androidPlugin.test.ts new file mode 100644 index 0000000000..a98daf5b30 --- /dev/null +++ b/packages/crashlytics/plugin/__tests__/androidPlugin.test.ts @@ -0,0 +1,31 @@ +import fs from 'fs/promises'; +import path from 'path'; + +import { applyPlugin } from '../src/android/applyPlugin'; +import { setBuildscriptDependency } from '../src/android/buildscriptDependency'; + +describe('Crashlytics 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 crashlytics classpath to project build.gradle', async function () { + const result = setBuildscriptDependency(projectBuildGradle); + expect(result).toMatchSnapshot(); + }); + + it('applies crashlytics plugin to app/build.gradle', async function () { + const result = applyPlugin(appBuildGradle); + expect(result).toMatchSnapshot(); + }); +}); diff --git a/packages/crashlytics/plugin/__tests__/fixtures/app_build.gradle b/packages/crashlytics/plugin/__tests__/fixtures/app_build.gradle new file mode 100644 index 0000000000..ce90569917 --- /dev/null +++ b/packages/crashlytics/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/crashlytics/plugin/__tests__/fixtures/project_build.gradle b/packages/crashlytics/plugin/__tests__/fixtures/project_build.gradle new file mode 100644 index 0000000000..4ff87f3b39 --- /dev/null +++ b/packages/crashlytics/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/crashlytics/plugin/src/android/applyPlugin.ts b/packages/crashlytics/plugin/src/android/applyPlugin.ts new file mode 100644 index 0000000000..1eed47efba --- /dev/null +++ b/packages/crashlytics/plugin/src/android/applyPlugin.ts @@ -0,0 +1,29 @@ +import { ConfigPlugin, WarningAggregator, withAppBuildGradle } from '@expo/config-plugins'; + +import { crashlyticsPlugin } from './constants'; + +/** + * Update `app/build.gradle` by applying crashlytics plugin + */ +export const withApplyCrashlyticsPlugin: ConfigPlugin = config => { + return withAppBuildGradle(config, config => { + if (config.modResults.language === 'groovy') { + config.modResults.contents = applyPlugin(config.modResults.contents); + } else { + WarningAggregator.addWarningAndroid( + 'react-native-firebase-crashlytics', + `Cannot automatically configure app build.gradle if it's not groovy`, + ); + } + return config; + }); +}; + +export function applyPlugin(appBuildGradle: string) { + const crashlyticsPattern = new RegExp(`apply\\s+plugin:\\s+['"]${crashlyticsPlugin}['"]`); + if (!appBuildGradle.match(crashlyticsPattern)) { + appBuildGradle += `\napply plugin: '${crashlyticsPlugin}'`; + } + + return appBuildGradle; +} diff --git a/packages/crashlytics/plugin/src/android/buildscriptDependency.ts b/packages/crashlytics/plugin/src/android/buildscriptDependency.ts new file mode 100644 index 0000000000..93992265a4 --- /dev/null +++ b/packages/crashlytics/plugin/src/android/buildscriptDependency.ts @@ -0,0 +1,33 @@ +import { ConfigPlugin, WarningAggregator, withProjectBuildGradle } from '@expo/config-plugins'; + +import { crashlyticsClassPath, crashlyticsVersion } from './constants'; + +/** + * Update `/build.gradle` by adding google-services 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-crashlytics', + `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(crashlyticsClassPath)) { + return buildGradle.replace( + /dependencies\s?{/, + `dependencies { + classpath '${crashlyticsClassPath}:${crashlyticsVersion}'`, + ); + } + + return buildGradle; +} diff --git a/packages/crashlytics/plugin/src/android/constants.ts b/packages/crashlytics/plugin/src/android/constants.ts new file mode 100644 index 0000000000..0a93b9ffb3 --- /dev/null +++ b/packages/crashlytics/plugin/src/android/constants.ts @@ -0,0 +1,5 @@ +const appPackageJson = require('@react-native-firebase/app/package.json'); + +export const crashlyticsClassPath = 'com.google.firebase:firebase-crashlytics-gradle'; +export const crashlyticsPlugin = 'com.google.firebase.crashlytics'; +export const crashlyticsVersion = appPackageJson.sdkVersions.android.firebaseCrashlyticsGradle; diff --git a/packages/crashlytics/plugin/src/android/index.ts b/packages/crashlytics/plugin/src/android/index.ts new file mode 100644 index 0000000000..e723cb14a1 --- /dev/null +++ b/packages/crashlytics/plugin/src/android/index.ts @@ -0,0 +1,4 @@ +import { withApplyCrashlyticsPlugin } from './applyPlugin'; +import { withBuildscriptDependency } from './buildscriptDependency'; + +export { withBuildscriptDependency, withApplyCrashlyticsPlugin }; diff --git a/packages/crashlytics/plugin/src/index.ts b/packages/crashlytics/plugin/src/index.ts new file mode 100644 index 0000000000..dec548ea35 --- /dev/null +++ b/packages/crashlytics/plugin/src/index.ts @@ -0,0 +1,13 @@ +import { ConfigPlugin, withPlugins, createRunOncePlugin } from '@expo/config-plugins'; + +import { withApplyCrashlyticsPlugin, withBuildscriptDependency } from './android'; + +/** + * A config plugin for configuring `@react-native-firebase/crashlytics` + */ +const withRnFirebaseCrashlytics: ConfigPlugin = config => { + return withPlugins(config, [withBuildscriptDependency, withApplyCrashlyticsPlugin]); +}; + +const pak = require('@react-native-firebase/crashlytics/package.json'); +export default createRunOncePlugin(withRnFirebaseCrashlytics, pak.name, pak.version); diff --git a/packages/crashlytics/plugin/tsconfig.json b/packages/crashlytics/plugin/tsconfig.json new file mode 100644 index 0000000000..a68b89f126 --- /dev/null +++ b/packages/crashlytics/plugin/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@tsconfig/node12/tsconfig.json", + "compilerOptions": { + "outDir": "build", + "rootDir": "src", + "declaration": true + }, + "include": ["./src"] +} diff --git a/packages/perf/app.plugin.js b/packages/perf/app.plugin.js new file mode 100644 index 0000000000..3c7d11b615 --- /dev/null +++ b/packages/perf/app.plugin.js @@ -0,0 +1 @@ +module.exports = require('./plugin/build'); diff --git a/packages/perf/package.json b/packages/perf/package.json index 971002addf..1e55095243 100644 --- a/packages/perf/package.json +++ b/packages/perf/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", @@ -29,6 +31,9 @@ "peerDependencies": { "@react-native-firebase/app": "12.3.0" }, + "dependencies": { + "@expo/config-plugins": "^3.0.2" + }, "publishConfig": { "access": "public" } diff --git a/packages/perf/plugin/__tests__/README.md b/packages/perf/plugin/__tests__/README.md new file mode 100644 index 0000000000..d4b1e159de --- /dev/null +++ b/packages/perf/plugin/__tests__/README.md @@ -0,0 +1 @@ +Please see the `packages/app/plugin/__tests__/README.md`. diff --git a/packages/perf/plugin/__tests__/__snapshots__/androidPlugin.test.ts.snap b/packages/perf/plugin/__tests__/__snapshots__/androidPlugin.test.ts.snap new file mode 100644 index 0000000000..70d4a9a6c1 --- /dev/null +++ b/packages/perf/plugin/__tests__/__snapshots__/androidPlugin.test.ts.snap @@ -0,0 +1,167 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Perf Monitoring Plugin Android Tests applies perf monitoring 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:perf-plugin:1.4.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[`Perf Monitoring Plugin Android Tests applies perf monitoring 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.firebase-perf'" +`; diff --git a/packages/perf/plugin/__tests__/androidPlugin.test.ts b/packages/perf/plugin/__tests__/androidPlugin.test.ts new file mode 100644 index 0000000000..c3a14b94de --- /dev/null +++ b/packages/perf/plugin/__tests__/androidPlugin.test.ts @@ -0,0 +1,31 @@ +import fs from 'fs/promises'; +import path from 'path'; + +import { applyPlugin } from '../src/android/applyPlugin'; +import { setBuildscriptDependency } from '../src/android/buildscriptDependency'; + +describe('Perf Monitoring 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 perf monitoring classpath to project build.gradle', async function () { + const result = setBuildscriptDependency(projectBuildGradle); + expect(result).toMatchSnapshot(); + }); + + it('applies perf monitoring plugin to app/build.gradle', async function () { + const result = applyPlugin(appBuildGradle); + expect(result).toMatchSnapshot(); + }); +}); diff --git a/packages/perf/plugin/__tests__/fixtures/app_build.gradle b/packages/perf/plugin/__tests__/fixtures/app_build.gradle new file mode 100644 index 0000000000..ce90569917 --- /dev/null +++ b/packages/perf/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/perf/plugin/__tests__/fixtures/project_build.gradle b/packages/perf/plugin/__tests__/fixtures/project_build.gradle new file mode 100644 index 0000000000..4ff87f3b39 --- /dev/null +++ b/packages/perf/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/perf/plugin/src/android/applyPlugin.ts b/packages/perf/plugin/src/android/applyPlugin.ts new file mode 100644 index 0000000000..feb939cb0b --- /dev/null +++ b/packages/perf/plugin/src/android/applyPlugin.ts @@ -0,0 +1,27 @@ +import { ConfigPlugin, WarningAggregator, withAppBuildGradle } from '@expo/config-plugins'; +import { perfMonitoringPlugin } from './constants'; + +/** + * Update `app/build.gradle` by applying performance monitoring plugin + */ +export const withApplyPerfPlugin: ConfigPlugin = config => { + return withAppBuildGradle(config, config => { + if (config.modResults.language === 'groovy') { + config.modResults.contents = applyPlugin(config.modResults.contents); + } else { + WarningAggregator.addWarningAndroid( + 'react-native-firebase-perf', + `Cannot automatically configure app build.gradle if it's not groovy`, + ); + } + return config; + }); +}; + +export function applyPlugin(appBuildGradle: string) { + const perfPattern = new RegExp(`apply\\s+plugin:\\s+['"]${perfMonitoringPlugin}['"]`); + if (!appBuildGradle.match(perfPattern)) { + appBuildGradle += `\napply plugin: '${perfMonitoringPlugin}'`; + } + return appBuildGradle; +} diff --git a/packages/perf/plugin/src/android/buildscriptDependency.ts b/packages/perf/plugin/src/android/buildscriptDependency.ts new file mode 100644 index 0000000000..f26bef2a1b --- /dev/null +++ b/packages/perf/plugin/src/android/buildscriptDependency.ts @@ -0,0 +1,33 @@ +import { ConfigPlugin, WarningAggregator, withProjectBuildGradle } from '@expo/config-plugins'; + +import { perfMonitoringClassPath, perfMonitoringVersion } from './constants'; + +/** + * Update `/build.gradle` by adding performance monitoring 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-perf', + `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(perfMonitoringClassPath)) { + return buildGradle.replace( + /dependencies\s?{/, + `dependencies { + classpath '${perfMonitoringClassPath}:${perfMonitoringVersion}'`, + ); + } + + return buildGradle; +} diff --git a/packages/perf/plugin/src/android/constants.ts b/packages/perf/plugin/src/android/constants.ts new file mode 100644 index 0000000000..67a3dfadfb --- /dev/null +++ b/packages/perf/plugin/src/android/constants.ts @@ -0,0 +1,5 @@ +const appPackageJson = require('@react-native-firebase/app/package.json'); + +export const perfMonitoringClassPath = 'com.google.firebase:perf-plugin'; +export const perfMonitoringPlugin = 'com.google.firebase.firebase-perf'; +export const perfMonitoringVersion = appPackageJson.sdkVersions.android.firebasePerfGradle; diff --git a/packages/perf/plugin/src/android/index.ts b/packages/perf/plugin/src/android/index.ts new file mode 100644 index 0000000000..534269158f --- /dev/null +++ b/packages/perf/plugin/src/android/index.ts @@ -0,0 +1,4 @@ +import { withApplyPerfPlugin } from './applyPlugin'; +import { withBuildscriptDependency } from './buildscriptDependency'; + +export { withBuildscriptDependency, withApplyPerfPlugin }; diff --git a/packages/perf/plugin/src/index.ts b/packages/perf/plugin/src/index.ts new file mode 100644 index 0000000000..724610a2b0 --- /dev/null +++ b/packages/perf/plugin/src/index.ts @@ -0,0 +1,13 @@ +import { ConfigPlugin, withPlugins, createRunOncePlugin } from '@expo/config-plugins'; + +import { withApplyPerfPlugin, withBuildscriptDependency } from './android'; + +/** + * A config plugin for configuring `@react-native-firebase/perf` + */ +const withRnFirebasePerf: ConfigPlugin = config => { + return withPlugins(config, [withBuildscriptDependency, withApplyPerfPlugin]); +}; + +const pak = require('@react-native-firebase/perf/package.json'); +export default createRunOncePlugin(withRnFirebasePerf, pak.name, pak.version); diff --git a/packages/perf/plugin/tsconfig.json b/packages/perf/plugin/tsconfig.json new file mode 100644 index 0000000000..a68b89f126 --- /dev/null +++ b/packages/perf/plugin/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@tsconfig/node12/tsconfig.json", + "compilerOptions": { + "outDir": "build", + "rootDir": "src", + "declaration": true + }, + "include": ["./src"] +} diff --git a/tsconfig.json b/tsconfig.json index 998e899c88..91a3288baf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,6 +17,7 @@ "skipLibCheck": false, "experimentalDecorators": true, "emitDecoratorMetadata": true, + "esModuleInterop": true, "lib": ["es2015", "es2016", "esnext"] }, "exclude": ["node_modules", "**/*.spec.ts"] diff --git a/yarn.lock b/yarn.lock index eeca26d194..76c05f1b58 100644 --- a/yarn.lock +++ b/yarn.lock @@ -25,6 +25,13 @@ dependencies: "@babel/highlight" "^7.12.13" +"@babel/code-frame@~7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" + integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== + dependencies: + "@babel/highlight" "^7.10.4" + "@babel/compat-data@^7.13.11", "@babel/compat-data@^7.13.12", "@babel/compat-data@^7.13.15", "@babel/compat-data@^7.13.8": version "7.13.15" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.13.15.tgz#7e8eea42d0b64fda2b375b22d06c605222e848f4" @@ -1203,6 +1210,48 @@ minimatch "^3.0.4" strip-json-comments "^3.1.1" +"@expo/config-plugins@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-3.0.2.tgz#3c9fb54164b48330494cd0172d21121686bddbc4" + integrity sha512-bS7t9JbPBK1lCcMo2mp7QSDq2a4xsc8E6sEYVwD+YS5RKx7nxEtSYohnFWlKthm624dHCaVzqEbKxhIGO3M3Uw== + dependencies: + "@expo/config-types" "^41.0.0" + "@expo/json-file" "8.2.30" + "@expo/plist" "0.0.13" + debug "^4.3.1" + find-up "~5.0.0" + fs-extra "9.0.0" + getenv "^1.0.0" + glob "7.1.6" + resolve-from "^5.0.0" + slash "^3.0.0" + xcode "^3.0.1" + xml2js "^0.4.23" + +"@expo/config-types@^41.0.0": + version "41.0.0" + resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-41.0.0.tgz#ffe1444c6c26e0e3a8f7149b4afe486e357536d1" + integrity sha512-Ax0pHuY5OQaSrzplOkT9DdpdmNzaVDnq9VySb4Ujq7UJ4U4jriLy8u93W98zunOXpcu0iiKubPsqD6lCiq0pig== + +"@expo/json-file@8.2.30": + version "8.2.30" + resolved "https://registry.yarnpkg.com/@expo/json-file/-/json-file-8.2.30.tgz#bd855b6416b5c3af7e55b43f6761c1e7d2b755b0" + integrity sha512-vrgGyPEXBoFI5NY70IegusCSoSVIFV3T3ry4tjJg1MFQKTUlR7E0r+8g8XR6qC705rc2PawaZQjqXMAVtV6s2A== + dependencies: + "@babel/code-frame" "~7.10.4" + fs-extra "9.0.0" + json5 "^1.0.1" + write-file-atomic "^2.3.0" + +"@expo/plist@0.0.13": + version "0.0.13" + resolved "https://registry.yarnpkg.com/@expo/plist/-/plist-0.0.13.tgz#700a48d9927aa2b0257c613e13454164e7371a96" + integrity sha512-zGPSq9OrCn7lWvwLLHLpHUUq2E40KptUFXn53xyZXPViI0k9lbApcR9KlonQZ95C+ELsf0BQ3gRficwK92Ivcw== + dependencies: + base64-js "^1.2.3" + xmlbuilder "^14.0.0" + xmldom "~0.5.0" + "@firebase/analytics-types@0.4.0": version "0.4.0" resolved "https://registry.yarnpkg.com/@firebase/analytics-types/-/analytics-types-0.4.0.tgz#d6716f9fa36a6e340bc0ecfe68af325aa6f60508" @@ -2923,6 +2972,11 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== +"@tsconfig/node12@^1.0.9": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.9.tgz#62c1f6dee2ebd9aead80dc3afa56810e58e1a04c" + integrity sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw== + "@types/archiver@^5.1.0": version "5.1.0" resolved "https://registry.yarnpkg.com/@types/archiver/-/archiver-5.1.0.tgz#869f4ce4028e49cf9a0243cf914415f4cc3d1f3d" @@ -3848,7 +3902,7 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -base64-js@^1.1.2, base64-js@^1.3.0, base64-js@^1.3.1, base64-js@^1.5.1: +base64-js@^1.1.2, base64-js@^1.2.3, base64-js@^1.3.0, base64-js@^1.3.1, base64-js@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== @@ -5191,6 +5245,13 @@ debug@^3.1.1: dependencies: ms "^2.1.1" +debug@^4.3.1: + version "4.3.2" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" + integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== + dependencies: + ms "2.1.2" + debuglog@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" @@ -6340,7 +6401,7 @@ find-replace@^1.0.2: array-back "^1.0.4" test-value "^2.1.0" -find-up@5.0.0: +find-up@5.0.0, find-up@~5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== @@ -6590,6 +6651,16 @@ fs-constants@^1.0.0: resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== +fs-extra@9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.0.0.tgz#b6afc31036e247b2466dc99c29ae797d5d4580a3" + integrity sha512-pmEYSk3vYsG/bF651KPUXZ+hvjpgWYw/Gc7W9NFUe3ZVLczKKWIij3IKpOrQcdw4TILtibFslZ0UmR8Vvzig4g== + dependencies: + at-least-node "^1.0.0" + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^1.0.0" + fs-extra@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-1.0.0.tgz#cd3ce5f7e7cb6145883fcae3191e9877f8587950" @@ -6832,6 +6903,11 @@ get-value@^2.0.3, get-value@^2.0.6: resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= +getenv@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/getenv/-/getenv-1.0.0.tgz#874f2e7544fbca53c7a4738f37de8605c3fcfc31" + integrity sha512-7yetJWqbS9sbn0vIfliPsFgoXMKn/YMF+Wuiog97x+urnSRRRZ7xB+uVkwGKzRgq9CDFfMQnE9ruL5DHv9c6Xg== + getpass@^0.1.1: version "0.1.7" resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" @@ -8639,6 +8715,13 @@ json5@2.x, json5@^2.1.2: dependencies: minimist "^1.2.5" +json5@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" + integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== + dependencies: + minimist "^1.2.0" + jsonfile@^2.1.0: version "2.4.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" @@ -12033,7 +12116,7 @@ sanitize-filename@^1.6.1: dependencies: truncate-utf8-bytes "^1.0.0" -sax@^1.2.1: +sax@>=0.6.0, sax@^1.2.1: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== @@ -12285,7 +12368,7 @@ signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== -simple-plist@^1.0.0: +simple-plist@^1.0.0, simple-plist@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/simple-plist/-/simple-plist-1.1.1.tgz#54367ca28bc5996a982c325c1c4a4c1a05f4047c" integrity sha512-pKMCVKvZbZTsqYR6RKgLfBHkh2cV89GXcA/0CVPje3sOiNOnXA8+rp/ciAMZ7JRaUdLzlEM6JFfUn+fS6Nt3hg== @@ -13538,6 +13621,11 @@ universalify@^0.1.0, universalify@^0.1.2: resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== +universalify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-1.0.0.tgz#b61a1da173e8435b2fe3c67d29b9adf8594bd16d" + integrity sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug== + universalify@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" @@ -13696,6 +13784,11 @@ uuid@^3.0.0, uuid@^3.0.1, uuid@^3.3.2, uuid@^3.3.3: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== +uuid@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b" + integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg== + uuid@^8.0.0, uuid@^8.3.0: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" @@ -14044,6 +14137,14 @@ xcode@^2.0.0: simple-plist "^1.0.0" uuid "^3.3.2" +xcode@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/xcode/-/xcode-3.0.1.tgz#3efb62aac641ab2c702458f9a0302696146aa53c" + integrity sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA== + dependencies: + simple-plist "^1.1.0" + uuid "^7.0.3" + xdg-basedir@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" @@ -14054,11 +14155,29 @@ xml-name-validator@^3.0.0: resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== +xml2js@^0.4.23: + version "0.4.23" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" + integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug== + dependencies: + sax ">=0.6.0" + xmlbuilder "~11.0.0" + +xmlbuilder@^14.0.0: + version "14.0.0" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-14.0.0.tgz#876b5aec4f05ffd5feb97b0a871c855d16fbeb8c" + integrity sha512-ts+B2rSe4fIckR6iquDjsKbQFK2NlUk6iG5nf14mDEyldgoc2nEKZ3jZWMPTxGQwVgToSjt6VGIho1H8/fNFTg== + xmlbuilder@^9.0.7: version "9.0.7" resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0= +xmlbuilder@~11.0.0: + version "11.0.1" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" + integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== + xmlchars@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" @@ -14071,7 +14190,7 @@ xmldoc@^1.1.2: dependencies: sax "^1.2.1" -xmldom@^0.5.0: +xmldom@^0.5.0, xmldom@~0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.5.0.tgz#193cb96b84aa3486127ea6272c4596354cb4962e" integrity sha512-Foaj5FXVzgn7xFzsKeNIde9g6aFBxTPi37iwsno8QvApmtg7KYrr+OPyRHcJF7dud2a5nGRBXK3n0dL62Gf7PA==