Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Added Expo config plugin #5480

Merged
merged 16 commits into from
Jul 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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