Skip to content

Commit

Permalink
feat: Add Expo config plugin (#5480)
Browse files Browse the repository at this point in the history
* Preparations & Added the `app` plugin
* Add perf monitoring plugin
* Add crashlytics plugin
* eslint ignore `plugin/build` dirs
* Add tests READMEs
  • Loading branch information
barthap authored Jul 22, 2021
1 parent 808a3f1 commit 832057c
Show file tree
Hide file tree
Showing 51 changed files with 1,888 additions and 8 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
src/version.js
packages/**/node_modules/**
packages/**/plugin/build/**
node_modules
scripts/
coverage
Expand Down
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package.json
packages/**/node_modules/**
packages/**/plugin/build/**
tests/node_modules/**
tests/app.playground.js
tests/app.smartreply.js
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions packages/app/app.plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('./plugin/build');
8 changes: 7 additions & 1 deletion packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -54,6 +56,7 @@
"react-native": "*"
},
"dependencies": {
"@expo/config-plugins": "^3.0.2",
"opencollective-postinstall": "^2.0.1",
"superstruct": "^0.6.2"
},
Expand All @@ -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"
}
}
Expand Down
19 changes: 19 additions & 0 deletions packages/app/plugin/__tests__/README.md
Original file line number Diff line number Diff line change
@@ -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
167 changes: 167 additions & 0 deletions packages/app/plugin/__tests__/__snapshots__/androidPlugin.test.ts.snap
Original file line number Diff line number Diff line change
@@ -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' }
}
}
"
`;
106 changes: 106 additions & 0 deletions packages/app/plugin/__tests__/__snapshots__/iosPlugin.test.ts.snap
Original file line number Diff line number Diff line change
@@ -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 <React/RCTBridge.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
#import <React/RCTLinkingManager.h>
#import <UMCore/UMModuleRegistry.h>
#import <UMReactNativeAdapter/UMNativeModulesProxy.h>
#import <UMReactNativeAdapter/UMModuleRegistryAdapter.h>
#import <EXSplashScreen/EXSplashScreenService.h>
#import <UMCore/UMModuleRegistryProvider.h>
#if defined(FB_SONARKIT_ENABLED) && __has_include(<FlipperKit/FlipperClient.h>)
#import <FlipperKit/FlipperClient.h>
#import <FlipperKitLayoutPlugin/FlipperKitLayoutPlugin.h>
#import <FlipperKitUserDefaultsPlugin/FKUserDefaultsPlugin.h>
#import <FlipperKitNetworkPlugin/FlipperKitNetworkPlugin.h>
#import <SKIOSNetworkPlugin/SKIOSNetworkAdapter.h>
#import <FlipperKitReactPlugin/FlipperKitReactPlugin.h>
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 () <RCTBridgeDelegate>
@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(<FlipperKit/FlipperClient.h>)
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<id<RCTBridgeModule>> *)extraModulesForBridge:(RCTBridge *)bridge
{
NSArray<id<RCTBridgeModule>> *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
"
`;
31 changes: 31 additions & 0 deletions packages/app/plugin/__tests__/androidPlugin.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
Loading

1 comment on commit 832057c

@vercel
Copy link

@vercel vercel bot commented on 832057c Jul 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.