Skip to content

Commit 832057c

Browse files
authored
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
1 parent 808a3f1 commit 832057c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+1888
-8
lines changed

.eslintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
src/version.js
22
packages/**/node_modules/**
3+
packages/**/plugin/build/**
34
node_modules
45
scripts/
56
coverage

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package.json
22
packages/**/node_modules/**
3+
packages/**/plugin/build/**
34
tests/node_modules/**
45
tests/app.playground.js
56
tests/app.smartreply.js

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"@babel/core": "^7.14.0",
4747
"@babel/preset-env": "7.14.1",
4848
"@octokit/core": "^3.3.1",
49+
"@tsconfig/node12": "^1.0.9",
4950
"@types/jest": "^26.0.23",
5051
"@types/react": "^17.0.5",
5152
"@types/react-native": "^0.64.4",

packages/app/app.plugin.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require('./plugin/build');

packages/app/package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
"build": "genversion --semi lib/version.js && npm run build:version",
1010
"build:version": "node ./scripts/genversion-ios && node ./scripts/genversion-android",
1111
"build:clean": "rimraf android/build && rimraf ios/build",
12-
"prepare": "npm run build"
12+
"build:plugin": "rimraf plugin/build && tsc --build plugin",
13+
"lint:plugin": "eslint plugin/src/*",
14+
"prepare": "npm run build && npm run build:plugin"
1315
},
1416
"repository": {
1517
"type": "git",
@@ -54,6 +56,7 @@
5456
"react-native": "*"
5557
},
5658
"dependencies": {
59+
"@expo/config-plugins": "^3.0.2",
5760
"opencollective-postinstall": "^2.0.1",
5861
"superstruct": "^0.6.2"
5962
},
@@ -70,6 +73,9 @@
7073
"compileSdk": 30,
7174
"buildTools": "30.0.2",
7275
"firebase": "28.2.1",
76+
"firebaseCrashlyticsGradle": "2.7.1",
77+
"firebasePerfGradle": "1.4.0",
78+
"gmsGoogleServicesGradle": "4.3.8",
7379
"playServicesAuth": "19.0.0"
7480
}
7581
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
## Expo Config Plugin unit tests
2+
3+
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:
4+
5+
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).
6+
2. Plugin changes are applied (e.g. gradle dependency is added).
7+
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.
8+
9+
You can preview the snapshot files manually, by opening `__snapshots__/*.snap` files.
10+
11+
### Updating the snapshots
12+
13+
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:
14+
15+
- Update all snapshots by running `npm run tests:jest -u`.
16+
- Update snapshots interactively, one by one:
17+
1. Run `yarn tests:jest --watchAll`
18+
2. Press `i` to let `jest` display changes and prompt you for updating each snapshot.
19+
> This option is not available, when there are no failing snapshots
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`Config Plugin Android Tests applies changes to app/build.gradle 1`] = `
4+
"/* Example build.gradle file from https://github.com/expo/expo/blob/6ab0274b5cb9a9c223e0d453787a522b438b4fcb/templates/expo-template-bare-minimum/android/app/build.gradle */
5+
6+
apply plugin: \\"com.android.application\\"
7+
8+
import com.android.build.OutputFile
9+
10+
11+
project.ext.react = [
12+
enableHermes: false
13+
]
14+
15+
apply from: '../../node_modules/react-native-unimodules/gradle.groovy'
16+
apply from: \\"../../node_modules/react-native/react.gradle\\"
17+
apply from: \\"../../node_modules/expo-constants/scripts/get-app-config-android.gradle\\"
18+
apply from: \\"../../node_modules/expo-updates/scripts/create-manifest-android.gradle\\"
19+
20+
def enableSeparateBuildPerCPUArchitecture = false
21+
22+
def enableProguardInReleaseBuilds = false
23+
24+
def jscFlavor = 'org.webkit:android-jsc:+'
25+
26+
def enableHermes = project.ext.react.get(\\"enableHermes\\", false);
27+
28+
android {
29+
compileSdkVersion rootProject.ext.compileSdkVersion
30+
31+
compileOptions {
32+
sourceCompatibility JavaVersion.VERSION_1_8
33+
targetCompatibility JavaVersion.VERSION_1_8
34+
}
35+
36+
defaultConfig {
37+
applicationId \\"com.helloworld\\"
38+
minSdkVersion rootProject.ext.minSdkVersion
39+
targetSdkVersion rootProject.ext.targetSdkVersion
40+
versionCode 1
41+
versionName \\"1.0\\"
42+
}
43+
splits {
44+
abi {
45+
reset()
46+
enable enableSeparateBuildPerCPUArchitecture
47+
universalApk false // If true, also generate a universal APK
48+
include \\"armeabi-v7a\\", \\"x86\\", \\"arm64-v8a\\", \\"x86_64\\"
49+
}
50+
}
51+
signingConfigs {
52+
debug {
53+
storeFile file('debug.keystore')
54+
storePassword 'android'
55+
keyAlias 'androiddebugkey'
56+
keyPassword 'android'
57+
}
58+
}
59+
buildTypes {
60+
debug {
61+
signingConfig signingConfigs.debug
62+
}
63+
release {
64+
// Caution! In production, you need to generate your own keystore file.
65+
// see https://reactnative.dev/docs/signed-apk-android.
66+
signingConfig signingConfigs.debug
67+
minifyEnabled enableProguardInReleaseBuilds
68+
proguardFiles getDefaultProguardFile(\\"proguard-android.txt\\"), \\"proguard-rules.pro\\"
69+
}
70+
}
71+
72+
// applicationVariants are e.g. debug, release
73+
applicationVariants.all { variant ->
74+
variant.outputs.each { output ->
75+
// For each separate APK per architecture, set a unique version code as described here:
76+
// https://developer.android.com/studio/build/configure-apk-splits.html
77+
def versionCodes = [\\"armeabi-v7a\\": 1, \\"x86\\": 2, \\"arm64-v8a\\": 3, \\"x86_64\\": 4]
78+
def abi = output.getFilter(OutputFile.ABI)
79+
if (abi != null) { // null for the universal-debug, universal-release variants
80+
output.versionCodeOverride =
81+
versionCodes.get(abi) * 1048576 + defaultConfig.versionCode
82+
}
83+
84+
}
85+
}
86+
}
87+
88+
dependencies {
89+
implementation fileTree(dir: \\"libs\\", include: [\\"*.jar\\"])
90+
//noinspection GradleDynamicVersion
91+
implementation \\"com.facebook.react:react-native:+\\" // From node_modules
92+
implementation \\"androidx.swiperefreshlayout:swiperefreshlayout:1.0.0\\"
93+
debugImplementation(\\"com.facebook.flipper:flipper:\${FLIPPER_VERSION}\\") {
94+
exclude group:'com.facebook.fbjni'
95+
}
96+
debugImplementation(\\"com.facebook.flipper:flipper-network-plugin:\${FLIPPER_VERSION}\\") {
97+
exclude group:'com.facebook.flipper'
98+
exclude group:'com.squareup.okhttp3', module:'okhttp'
99+
}
100+
debugImplementation(\\"com.facebook.flipper:flipper-fresco-plugin:\${FLIPPER_VERSION}\\") {
101+
exclude group:'com.facebook.flipper'
102+
}
103+
addUnimodulesDependencies()
104+
105+
if (enableHermes) {
106+
def hermesPath = \\"../../node_modules/hermes-engine/android/\\";
107+
debugImplementation files(hermesPath + \\"hermes-debug.aar\\")
108+
releaseImplementation files(hermesPath + \\"hermes-release.aar\\")
109+
} else {
110+
implementation jscFlavor
111+
}
112+
}
113+
114+
// Run this once to be able to run the application with BUCK
115+
// puts all compile dependencies into folder libs for BUCK to use
116+
task copyDownloadableDepsToLibs(type: Copy) {
117+
from configurations.compile
118+
into 'libs'
119+
}
120+
121+
apply from: file(\\"../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle\\"); applyNativeModulesAppBuildGradle(project)
122+
123+
apply plugin: 'com.google.gms.google-services'"
124+
`;
125+
126+
exports[`Config Plugin Android Tests applies changes to project build.gradle 1`] = `
127+
"// Top-level build file where you can add configuration options common to all sub-projects/modules.
128+
129+
buildscript {
130+
ext {
131+
buildToolsVersion = \\"29.0.3\\"
132+
minSdkVersion = 21
133+
compileSdkVersion = 30
134+
targetSdkVersion = 30
135+
}
136+
repositories {
137+
google()
138+
jcenter()
139+
}
140+
dependencies {
141+
classpath 'com.google.gms:google-services:4.3.8'
142+
classpath(\\"com.android.tools.build:gradle:4.1.0\\")
143+
144+
// NOTE: Do not place your application dependencies here; they belong
145+
// in the individual module build.gradle files
146+
}
147+
}
148+
149+
allprojects {
150+
repositories {
151+
mavenLocal()
152+
maven {
153+
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
154+
url(\\"$rootDir/../node_modules/react-native/android\\")
155+
}
156+
maven {
157+
// Android JSC is installed from npm
158+
url(\\"$rootDir/../node_modules/jsc-android/dist\\")
159+
}
160+
161+
google()
162+
jcenter()
163+
maven { url 'https://www.jitpack.io' }
164+
}
165+
}
166+
"
167+
`;
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`Config Plugin iOS Tests tests changes made to AppDelegate.m 1`] = `
4+
"#import \\"AppDelegate.h\\"
5+
@import Firebase;
6+
7+
#import <React/RCTBridge.h>
8+
#import <React/RCTBundleURLProvider.h>
9+
#import <React/RCTRootView.h>
10+
#import <React/RCTLinkingManager.h>
11+
12+
#import <UMCore/UMModuleRegistry.h>
13+
#import <UMReactNativeAdapter/UMNativeModulesProxy.h>
14+
#import <UMReactNativeAdapter/UMModuleRegistryAdapter.h>
15+
#import <EXSplashScreen/EXSplashScreenService.h>
16+
#import <UMCore/UMModuleRegistryProvider.h>
17+
18+
#if defined(FB_SONARKIT_ENABLED) && __has_include(<FlipperKit/FlipperClient.h>)
19+
#import <FlipperKit/FlipperClient.h>
20+
#import <FlipperKitLayoutPlugin/FlipperKitLayoutPlugin.h>
21+
#import <FlipperKitUserDefaultsPlugin/FKUserDefaultsPlugin.h>
22+
#import <FlipperKitNetworkPlugin/FlipperKitNetworkPlugin.h>
23+
#import <SKIOSNetworkPlugin/SKIOSNetworkAdapter.h>
24+
#import <FlipperKitReactPlugin/FlipperKitReactPlugin.h>
25+
26+
static void InitializeFlipper(UIApplication *application) {
27+
FlipperClient *client = [FlipperClient sharedClient];
28+
SKDescriptorMapper *layoutDescriptorMapper = [[SKDescriptorMapper alloc] initWithDefaults];
29+
[client addPlugin:[[FlipperKitLayoutPlugin alloc] initWithRootNode:application withDescriptorMapper:layoutDescriptorMapper]];
30+
[client addPlugin:[[FKUserDefaultsPlugin alloc] initWithSuiteName:nil]];
31+
[client addPlugin:[FlipperKitReactPlugin new]];
32+
[client addPlugin:[[FlipperKitNetworkPlugin alloc] initWithNetworkAdapter:[SKIOSNetworkAdapter new]]];
33+
[client start];
34+
}
35+
#endif
36+
37+
@interface AppDelegate () <RCTBridgeDelegate>
38+
39+
@property (nonatomic, strong) UMModuleRegistryAdapter *moduleRegistryAdapter;
40+
@property (nonatomic, strong) NSDictionary *launchOptions;
41+
42+
@end
43+
44+
@implementation AppDelegate
45+
46+
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
47+
{
48+
#if defined(FB_SONARKIT_ENABLED) && __has_include(<FlipperKit/FlipperClient.h>)
49+
InitializeFlipper(application);
50+
#endif
51+
52+
[FIRApp configure];
53+
self.moduleRegistryAdapter = [[UMModuleRegistryAdapter alloc] initWithModuleRegistryProvider:[[UMModuleRegistryProvider alloc] init]];
54+
self.launchOptions = launchOptions;
55+
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
56+
#ifdef DEBUG
57+
[self initializeReactNativeApp];
58+
#else
59+
EXUpdatesAppController *controller = [EXUpdatesAppController sharedInstance];
60+
controller.delegate = self;
61+
[controller startAndShowLaunchScreen:self.window];
62+
#endif
63+
64+
[super application:application didFinishLaunchingWithOptions:launchOptions];
65+
66+
return YES;
67+
}
68+
69+
- (RCTBridge *)initializeReactNativeApp
70+
{
71+
RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:self.launchOptions];
72+
RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:@\\"main\\" initialProperties:nil];
73+
rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1];
74+
75+
UIViewController *rootViewController = [UIViewController new];
76+
rootViewController.view = rootView;
77+
self.window.rootViewController = rootViewController;
78+
[self.window makeKeyAndVisible];
79+
80+
return bridge;
81+
}
82+
83+
- (NSArray<id<RCTBridgeModule>> *)extraModulesForBridge:(RCTBridge *)bridge
84+
{
85+
NSArray<id<RCTBridgeModule>> *extraModules = [_moduleRegistryAdapter extraModulesForBridge:bridge];
86+
// If you'd like to export some custom RCTBridgeModules that are not Expo modules, add them here!
87+
return extraModules;
88+
}
89+
90+
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge {
91+
#ifdef DEBUG
92+
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@\\"index\\" fallbackResource:nil];
93+
#else
94+
return [[EXUpdatesAppController sharedInstance] launchAssetUrl];
95+
#endif
96+
}
97+
98+
- (void)appController:(EXUpdatesAppController *)appController didStartWithSuccess:(BOOL)success {
99+
appController.bridge = [self initializeReactNativeApp];
100+
EXSplashScreenService *splashScreenService = (EXSplashScreenService *)[UMModuleRegistryProvider getSingletonModuleForClass:[EXSplashScreenService class]];
101+
[splashScreenService showSplashScreenFor:self.window.rootViewController];
102+
}
103+
104+
@end
105+
"
106+
`;
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import fs from 'fs/promises';
2+
import path from 'path';
3+
4+
import { applyPlugin } from '../src/android/applyPlugin';
5+
import { setBuildscriptDependency } from '../src/android/buildscriptDependency';
6+
7+
describe('Config Plugin Android Tests', function () {
8+
let appBuildGradle: string;
9+
let projectBuildGradle: string;
10+
11+
beforeAll(async function () {
12+
projectBuildGradle = await fs.readFile(
13+
path.resolve(__dirname, './fixtures/project_build.gradle'),
14+
{ encoding: 'utf-8' },
15+
);
16+
17+
appBuildGradle = await fs.readFile(path.resolve(__dirname, './fixtures/app_build.gradle'), {
18+
encoding: 'utf-8',
19+
});
20+
});
21+
22+
it('applies changes to project build.gradle', async function () {
23+
const result = setBuildscriptDependency(projectBuildGradle);
24+
expect(result).toMatchSnapshot();
25+
});
26+
27+
it('applies changes to app/build.gradle', async function () {
28+
const result = applyPlugin(appBuildGradle);
29+
expect(result).toMatchSnapshot();
30+
});
31+
});

0 commit comments

Comments
 (0)