diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index dc501136c..d1f3043ba 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -54,3 +54,35 @@ jobs:
pod install
../scripts/xcodebuild-ios.sh TemplateExample.xcworkspace build
working-directory: template-example
+ Android:
+ strategy:
+ matrix:
+ os: [macos-latest, windows-latest]
+ runs-on: ${{ matrix.os }}
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+ - name: Set up JDK
+ uses: actions/setup-java@v1
+ with:
+ java-version: 1.8
+ - name: Set up Node
+ uses: actions/setup-node@v1
+ with:
+ # node has a bug where it crashes compiling a regular
+ # expression of react-native cli. Using a specific
+ # node version helps to workaround this problem:
+ # https://github.com/facebook/react-native/issues/26598
+ node-version: 12.9.1
+ - name: Install
+ run: |
+ yarn
+ working-directory: example
+ - name: Build
+ shell: bash
+ run: |
+ set -eo pipefail
+ yarn build:android
+ ./gradlew clean build check test
+ working-directory: example
+
\ No newline at end of file
diff --git a/android/app/build.gradle b/android/app/build.gradle
index d60b436fe..fd61f0036 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -1,11 +1,40 @@
-import java.nio.file.Paths
+buildscript {
+ ext.kotlinVersion = "1.3.70"
-apply plugin: 'com.android.application'
-apply plugin: 'kotlin-android'
-apply plugin: 'kotlin-android-extensions'
+ def buildscriptDir = buildscript.sourceFile.getParent()
+ apply from: "$buildscriptDir/../test-app-util.gradle"
-def buildscriptDir = buildscript.sourceFile.getParent()
-apply from: "$buildscriptDir/../../test-app-util.gradle"
+ repositories {
+ jcenter()
+ google()
+ }
+
+ dependencies {
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
+ classpath "com.android.tools.build:gradle:3.6.1"
+ }
+}
+
+repositories {
+ maven {
+ url("${findNodeModulesPath(rootDir, "react-native")}/android")
+ }
+
+ jcenter()
+ google()
+}
+
+apply plugin: "com.android.application"
+apply plugin: "kotlin-android"
+apply plugin: "kotlin-android-extensions"
+apply plugin: "kotlin-kapt"
+
+def testAppDir = file("$projectDir/../../")
+
+apply from: file("${testAppDir}/test-app.gradle")
+applyTestAppModule(project, "com.sample")
+
+project.ext.react = [enableHermes: true]
android {
compileSdkVersion 29
@@ -19,20 +48,43 @@ android {
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
+
+ packagingOptions {
+ pickFirst "lib/armeabi-v7a/libc++_shared.so"
+ pickFirst "lib/arm64-v8a/libc++_shared.so"
+ pickFirst "lib/x86_64/libc++_shared.so"
+ pickFirst "lib/x86/libc++_shared.so"
+ }
}
-def hermesEnginePath = findNodeModulePath("hermes-engine")
+// TODO: switch back to using path below when running on react-native v0.61.5
+// def hermesEnginePath = findNodeModulesPath(projectDir, "hermes-engine")
+def hermesEnginePath = findNodeModulesPath(projectDir, "hermesvm")
def hermesPath = "$hermesEnginePath/android"
dependencies {
- debugImplementation files("$hermesPath/hermes-debug.aar")
+ implementation "com.google.dagger:dagger:2.27"
+ implementation "com.google.dagger:dagger-android:2.27"
+ implementation "com.google.dagger:dagger-android-support:2.27"
+
+ kapt("com.google.dagger:dagger-compiler:2.27")
+ kapt("com.google.dagger:dagger-android-processor:2.27")
+
releaseImplementation files("$hermesPath/hermes-release.aar")
+ debugImplementation files("$hermesPath/hermes-debug.aar")
implementation "com.facebook.react:react-native:+"
- implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
- implementation 'androidx.appcompat:appcompat:1.0.2'
- implementation 'androidx.core:core-ktx:1.0.2'
- testImplementation 'junit:junit:4.12'
- androidTestImplementation 'androidx.test.ext:junit:1.1.0'
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
+
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
+ implementation "androidx.appcompat:appcompat:1.1.0"
+ implementation "androidx.core:core-ktx:1.2.0"
+ implementation "androidx.recyclerview:recyclerview:1.1.0"
+ implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
+
+ implementation("com.squareup.moshi:moshi-kotlin:1.9.2")
+ kapt("com.squareup.moshi:moshi-kotlin-codegen:1.9.2")
+
+ testImplementation "junit:junit:4.13"
+ androidTestImplementation "androidx.test.ext:junit:1.1.1"
+ androidTestImplementation "androidx.test.espresso:espresso-core:3.2.0"
}
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 7aeee2f5e..9903d0c3b 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -5,6 +5,7 @@
+
+
diff --git a/android/app/src/main/java/com/sample/ComponentActivity.kt b/android/app/src/main/java/com/sample/ComponentActivity.kt
new file mode 100644
index 000000000..dcfcbe0a1
--- /dev/null
+++ b/android/app/src/main/java/com/sample/ComponentActivity.kt
@@ -0,0 +1,29 @@
+package com.sample
+
+import android.app.Activity
+import android.content.Intent
+import android.os.Bundle
+import com.facebook.react.ReactActivity
+import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler
+import com.facebook.soloader.SoLoader
+
+class ComponentActivity : ReactActivity(), DefaultHardwareBackBtnHandler {
+ companion object {
+ private const val COMPONENT_NAME = "extra:componentName";
+
+ fun newIntent(activity: Activity, componentName: String): Intent {
+ return Intent(activity, ComponentActivity::class.java).apply {
+ putExtra(COMPONENT_NAME, componentName)
+ }
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ SoLoader.init(this, false)
+
+ val componentName = intent.getStringExtra(COMPONENT_NAME)
+ loadApp(componentName)
+ }
+}
diff --git a/android/app/src/main/java/com/sample/ComponentListAdapter.kt b/android/app/src/main/java/com/sample/ComponentListAdapter.kt
new file mode 100644
index 000000000..cc4b91b2b
--- /dev/null
+++ b/android/app/src/main/java/com/sample/ComponentListAdapter.kt
@@ -0,0 +1,37 @@
+package com.sample
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+
+class ComponentListAdapter(
+ private val layoutInflater: LayoutInflater,
+ private val components: List,
+ private val listener: (ComponentViewModel) -> Unit
+) : RecyclerView.Adapter() {
+
+ override fun getItemCount() = components.size
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ComponentViewHolder {
+ return ComponentViewHolder(
+ layoutInflater.inflate(
+ R.layout.recyclerview_item_component, parent, false
+ ) as TextView
+ )
+ }
+
+ override fun onBindViewHolder(holder: ComponentViewHolder, position: Int) {
+ holder.bindTo(components[position])
+ }
+
+ inner class ComponentViewHolder(private val view: TextView) : RecyclerView.ViewHolder(view) {
+ init {
+ view.setOnClickListener { listener(components[adapterPosition]) }
+ }
+
+ fun bindTo(component: ComponentViewModel) {
+ view.text = component.displayName
+ }
+ }
+}
diff --git a/android/app/src/main/java/com/sample/ComponentViewModel.kt b/android/app/src/main/java/com/sample/ComponentViewModel.kt
new file mode 100644
index 000000000..bc4cce96c
--- /dev/null
+++ b/android/app/src/main/java/com/sample/ComponentViewModel.kt
@@ -0,0 +1,3 @@
+package com.sample
+
+data class ComponentViewModel(val name: String, val displayName: String)
diff --git a/android/app/src/main/java/com/sample/MainActivity.kt b/android/app/src/main/java/com/sample/MainActivity.kt
index e39fc6e1f..b3744374e 100644
--- a/android/app/src/main/java/com/sample/MainActivity.kt
+++ b/android/app/src/main/java/com/sample/MainActivity.kt
@@ -1,77 +1,43 @@
package com.sample
import android.os.Bundle
-import android.view.KeyEvent
+import android.view.LayoutInflater
import androidx.appcompat.app.AppCompatActivity
-import com.facebook.react.PackageList
-import com.facebook.react.ReactInstanceManager
-import com.facebook.react.ReactRootView
-import com.facebook.react.TestAppPackageList
-import com.facebook.react.common.LifecycleState
-import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler
-import com.facebook.soloader.SoLoader
+import androidx.recyclerview.widget.DividerItemDecoration
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.sample.manifest.ManifestProvider
+import dagger.android.AndroidInjection
+import javax.inject.Inject
-class MainActivity : AppCompatActivity(), DefaultHardwareBackBtnHandler {
- private lateinit var reactRootView: ReactRootView
- private lateinit var reactInstanceManager: ReactInstanceManager
+class MainActivity : AppCompatActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- SoLoader.init(this, false)
-
- reactRootView = ReactRootView(this)
- setContentView(reactRootView)
-
- reactInstanceManager = ReactInstanceManager.builder()
- .setInitialLifecycleState(LifecycleState.BEFORE_RESUME)
- .addPackages(PackageList(application).packages)
- .addPackages(TestAppPackageList().packages)
- .setUseDeveloperSupport(BuildConfig.DEBUG)
- .setCurrentActivity(this)
- .setBundleAssetName("index.android.bundle")
- .setJSMainModulePath("index")
- .setApplication(application)
- .build()
+ @Inject
+ lateinit var manifestProvider: ManifestProvider
- reactRootView.startReactApplication(
- reactInstanceManager, "TestComponent", null
- )
+ private val listener = { component: ComponentViewModel ->
+ startActivity(ComponentActivity.newIntent(this, component.name))
}
- override fun invokeDefaultOnBackPressed() {
- onBackPressed()
- }
+ override fun onCreate(savedInstanceState: Bundle?) {
+ AndroidInjection.inject(this)
- override fun onBackPressed() {
- reactInstanceManager.onBackPressed()
- }
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
- override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
- if (keyCode == KeyEvent.KEYCODE_MENU) {
- reactInstanceManager.showDevOptionsDialog()
- return true
+ val manifest = manifestProvider.manifest
+ ?: throw IllegalStateException("app.json is not provided or TestApp is misconfigured")
+ val components = manifest.components.map {
+ ComponentViewModel(it.key, it.value.displayName)
}
- return super.onKeyUp(keyCode, event)
- }
-
-
- override fun onPause() {
- super.onPause()
-
- reactInstanceManager.onHostPause(this)
- }
-
- override fun onResume() {
- super.onResume()
- reactInstanceManager.onHostResume(this, this)
- }
+ supportActionBar?.title = manifest.displayName
- override fun onDestroy() {
- super.onDestroy()
+ findViewById(R.id.recyclerview).apply {
+ layoutManager = LinearLayoutManager(context)
+ adapter = ComponentListAdapter(LayoutInflater.from(context), components, listener)
- reactInstanceManager.onHostDestroy(this)
- reactRootView.unmountReactApplication()
+ addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
+ }
}
}
diff --git a/android/app/src/main/java/com/sample/TestApp.kt b/android/app/src/main/java/com/sample/TestApp.kt
new file mode 100644
index 000000000..c23b41317
--- /dev/null
+++ b/android/app/src/main/java/com/sample/TestApp.kt
@@ -0,0 +1,33 @@
+package com.sample
+
+import android.app.Application
+import com.facebook.react.ReactApplication
+import com.facebook.react.ReactNativeHost
+import com.sample.di.DaggerTestAppComponent
+import dagger.android.AndroidInjector
+import dagger.android.DispatchingAndroidInjector
+import dagger.android.HasAndroidInjector
+import javax.inject.Inject
+
+class TestApp : Application(), HasAndroidInjector, ReactApplication {
+
+ @Inject
+ lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector
+
+ @Inject
+ lateinit var reactNativeHostInternal: ReactNativeHost
+
+ override fun onCreate() {
+ super.onCreate()
+
+ val testAppComponent = DaggerTestAppComponent.builder()
+ .binds(this)
+ .build()
+
+ testAppComponent.inject(this)
+ }
+
+ override fun androidInjector(): AndroidInjector = dispatchingAndroidInjector
+
+ override fun getReactNativeHost() = reactNativeHostInternal
+}
diff --git a/android/app/src/main/java/com/sample/di/ActivityScope.kt b/android/app/src/main/java/com/sample/di/ActivityScope.kt
new file mode 100644
index 000000000..28caee927
--- /dev/null
+++ b/android/app/src/main/java/com/sample/di/ActivityScope.kt
@@ -0,0 +1,7 @@
+package com.sample.di
+
+import javax.inject.Scope
+
+@Scope
+@Retention(AnnotationRetention.RUNTIME)
+annotation class ActivityScope
diff --git a/android/app/src/main/java/com/sample/di/TestAppBindings.kt b/android/app/src/main/java/com/sample/di/TestAppBindings.kt
new file mode 100644
index 000000000..01c28c95b
--- /dev/null
+++ b/android/app/src/main/java/com/sample/di/TestAppBindings.kt
@@ -0,0 +1,24 @@
+package com.sample.di
+
+import android.app.Application
+import android.content.Context
+import com.facebook.react.ReactNativeHost
+import com.sample.MainActivity
+import com.sample.react.TestAppReactNativeHost
+import dagger.Binds
+import dagger.Module
+import dagger.android.ContributesAndroidInjector
+
+@Module
+abstract class TestAppBindings {
+
+ @Binds
+ abstract fun bindsContext(application: Application): Context
+
+ @Binds
+ abstract fun bindsReactNativeHost(reactNativeHost: TestAppReactNativeHost): ReactNativeHost
+
+ @ActivityScope
+ @ContributesAndroidInjector
+ abstract fun contributeMainActivityInjector(): MainActivity
+}
diff --git a/android/app/src/main/java/com/sample/di/TestAppComponent.kt b/android/app/src/main/java/com/sample/di/TestAppComponent.kt
new file mode 100644
index 000000000..1841e26d0
--- /dev/null
+++ b/android/app/src/main/java/com/sample/di/TestAppComponent.kt
@@ -0,0 +1,30 @@
+package com.sample.di
+
+import android.app.Application
+import com.sample.TestApp
+import com.sample.manifest.ManifestModule
+import dagger.BindsInstance
+import dagger.Component
+import dagger.android.AndroidInjectionModule
+import javax.inject.Singleton
+
+@Singleton
+@Component(
+ modules = [
+ ManifestModule::class,
+ TestAppBindings::class,
+ AndroidInjectionModule::class
+ ]
+)
+interface TestAppComponent {
+ fun inject(testApp: TestApp)
+
+ @Component.Builder
+ interface Builder {
+
+ @BindsInstance
+ fun binds(application: Application): Builder
+
+ fun build(): TestAppComponent
+ }
+}
diff --git a/android/app/src/main/java/com/sample/manifest/Manifest.kt b/android/app/src/main/java/com/sample/manifest/Manifest.kt
new file mode 100644
index 000000000..642d8a86e
--- /dev/null
+++ b/android/app/src/main/java/com/sample/manifest/Manifest.kt
@@ -0,0 +1,15 @@
+package com.sample.manifest
+
+import com.squareup.moshi.JsonClass
+
+typealias Components = Map
+
+@JsonClass(generateAdapter = true)
+data class Manifest(
+ val name: String,
+ val displayName: String,
+ val components: Components
+)
+
+@JsonClass(generateAdapter = true)
+data class Component(val displayName: String)
diff --git a/android/app/src/main/java/com/sample/manifest/ManifestModule.kt b/android/app/src/main/java/com/sample/manifest/ManifestModule.kt
new file mode 100644
index 000000000..778a6ca44
--- /dev/null
+++ b/android/app/src/main/java/com/sample/manifest/ManifestModule.kt
@@ -0,0 +1,20 @@
+package com.sample.manifest
+
+import com.squareup.moshi.JsonAdapter
+import com.squareup.moshi.Moshi
+import dagger.Module
+import dagger.Provides
+import javax.inject.Singleton
+
+@Module
+class ManifestModule {
+
+ @Provides
+ @Singleton
+ fun providesMoshi(): Moshi = Moshi.Builder().build()
+
+ @Provides
+ @Singleton
+ fun providesManifestMoshiAdapter(moshi: Moshi): JsonAdapter =
+ ManifestJsonAdapter(moshi)
+}
diff --git a/android/app/src/main/java/com/sample/manifest/ManifestProvider.kt b/android/app/src/main/java/com/sample/manifest/ManifestProvider.kt
new file mode 100644
index 000000000..c0489063b
--- /dev/null
+++ b/android/app/src/main/java/com/sample/manifest/ManifestProvider.kt
@@ -0,0 +1,22 @@
+package com.sample.manifest
+
+import android.content.Context
+import com.sample.R
+import com.squareup.moshi.JsonAdapter
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class ManifestProvider @Inject constructor(
+ private val context: Context,
+ private val adapter: JsonAdapter
+) {
+ val manifest: Manifest? by lazy {
+ val appJson = context.resources
+ .openRawResource(R.raw.app)
+ .bufferedReader()
+ .use { it.readText() }
+
+ adapter.fromJson(appJson)
+ }
+}
diff --git a/android/app/src/main/java/com/sample/react/ReactBundleNameProvider.kt b/android/app/src/main/java/com/sample/react/ReactBundleNameProvider.kt
new file mode 100644
index 000000000..d6530e6aa
--- /dev/null
+++ b/android/app/src/main/java/com/sample/react/ReactBundleNameProvider.kt
@@ -0,0 +1,24 @@
+package com.sample.react
+
+import android.content.Context
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class ReactBundleNameProvider @Inject constructor(private val context: Context) {
+ val bundleName: String? by lazy {
+ val possibleEntryFiles = listOf(
+ "index.android",
+ "main.android",
+ "index.mobile",
+ "main.mobile",
+ "index.native",
+ "main.native",
+ "index",
+ "main"
+ ).map { "$it.jsbundle" }
+
+ context.resources.assets.list("")
+ ?.firstOrNull { possibleEntryFiles.contains(it) }
+ }
+}
diff --git a/android/app/src/main/java/com/sample/react/TestAppReactNativeHost.kt b/android/app/src/main/java/com/sample/react/TestAppReactNativeHost.kt
new file mode 100644
index 000000000..29dbb0ec0
--- /dev/null
+++ b/android/app/src/main/java/com/sample/react/TestAppReactNativeHost.kt
@@ -0,0 +1,21 @@
+package com.sample.react
+
+import android.app.Application
+import com.facebook.react.PackageList
+import com.facebook.react.ReactNativeHost
+import com.facebook.react.ReactPackage
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class TestAppReactNativeHost @Inject constructor(
+ application: Application,
+ private val reactBundleNameProvider: ReactBundleNameProvider
+) : ReactNativeHost(application) {
+
+ override fun getBundleAssetName() = reactBundleNameProvider.bundleName
+
+ override fun getUseDeveloperSupport() = bundleAssetName != null
+
+ override fun getPackages(): List = PackageList(application).packages
+}
diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 000000000..deed7c24b
--- /dev/null
+++ b/android/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,9 @@
+
+
diff --git a/android/app/src/main/res/layout/recyclerview_item_component.xml b/android/app/src/main/res/layout/recyclerview_item_component.xml
new file mode 100644
index 000000000..486891674
--- /dev/null
+++ b/android/app/src/main/res/layout/recyclerview_item_component.xml
@@ -0,0 +1,15 @@
+
+
diff --git a/android/build.gradle b/android/build.gradle
deleted file mode 100644
index 2f91a5166..000000000
--- a/android/build.gradle
+++ /dev/null
@@ -1,24 +0,0 @@
-buildscript {
- ext.kotlin_version = '1.3.61'
-
- repositories {
- google()
- jcenter()
- }
-
- dependencies {
- classpath 'com.android.tools.build:gradle:3.5.3'
- classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
- }
-}
-
-allprojects {
- repositories {
- google()
- jcenter()
- }
-}
-
-task clean(type: Delete) {
- delete rootProject.buildDir
-}
diff --git a/android/test-app-native-modules.gradle b/android/test-app-native-modules.gradle
new file mode 100644
index 000000000..240863d6c
--- /dev/null
+++ b/android/test-app-native-modules.gradle
@@ -0,0 +1,307 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2018 react-native-community
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+// TODO: https://github.com/microsoft/react-native-test-app/issues/31
+
+import groovy.json.JsonSlurper
+import org.gradle.initialization.DefaultSettings
+
+def jsAppDir = buildscript.sourceFile.toString().split("node_modules(/|\\\\)@react-native-community(/|\\\\)cli-platform-android")[0]
+def generatedFileName = "PackageList.java"
+def generatedFilePackage = "com.facebook.react"
+def generatedFileContentsTemplate = """
+package $generatedFilePackage;
+
+import android.app.Application;
+import android.content.Context;
+import android.content.res.Resources;
+
+import com.facebook.react.ReactPackage;
+import com.facebook.react.shell.MainPackageConfig;
+import com.facebook.react.shell.MainReactPackage;
+import java.util.Arrays;
+import java.util.ArrayList;
+
+{{ packageImports }}
+
+public class PackageList {
+ private Application application;
+ private ReactNativeHost reactNativeHost;
+ private MainPackageConfig mConfig;
+
+ public PackageList(ReactNativeHost reactNativeHost) {
+ this(reactNativeHost, null);
+ }
+
+ public PackageList(Application application) {
+ this(application, null);
+ }
+
+ public PackageList(ReactNativeHost reactNativeHost, MainPackageConfig config) {
+ this.reactNativeHost = reactNativeHost;
+ mConfig = config;
+ }
+
+ public PackageList(Application application, MainPackageConfig config) {
+ this.reactNativeHost = null;
+ this.application = application;
+ mConfig = config;
+ }
+
+ private ReactNativeHost getReactNativeHost() {
+ return this.reactNativeHost;
+ }
+
+ private Resources getResources() {
+ return this.getApplication().getResources();
+ }
+
+ private Application getApplication() {
+ if (this.reactNativeHost == null) return this.application;
+ return this.reactNativeHost.getApplication();
+ }
+
+ private Context getApplicationContext() {
+ return this.getApplication().getApplicationContext();
+ }
+
+ public ArrayList getPackages() {
+ return new ArrayList<>(Arrays.asList(
+ new MainReactPackage(mConfig){{ packageClassInstances }}
+ ));
+ }
+}
+"""
+
+class ReactNativeModules {
+ private Logger logger
+ private String jsAppDir
+ private ArrayList> reactNativeModules
+
+ private static String LOG_PREFIX = ":ReactNative:"
+ private static String REACT_NATIVE_CLI_BIN = "node_modules${File.separator}" +
+ "@react-native-community${File.separator}" +
+ "cli${File.separator}" +
+ "build${File.separator}" +
+ "index.js"
+
+ ReactNativeModules(Logger logger, String jsAppDir) {
+ this.logger = logger
+ this.jsAppDir = jsAppDir
+ this.reactNativeModules = this.getReactNativeConfig()
+ }
+
+ /**
+ * Include the react native modules android projects and specify their project directory
+ */
+ void addReactNativeModuleProjects(DefaultSettings defaultSettings) {
+ reactNativeModules.forEach { reactNativeModule ->
+ String nameCleansed = reactNativeModule["nameCleansed"]
+ String androidSourceDir = reactNativeModule["androidSourceDir"]
+
+ defaultSettings.include(":${nameCleansed}")
+ defaultSettings.project(":${nameCleansed}").projectDir = new File("${androidSourceDir}")
+ }
+ }
+
+ /**
+ * Adds the react native modules as dependencies to the users `app` project
+ */
+ void addReactNativeModuleDependencies(Project appProject) {
+ reactNativeModules.forEach { reactNativeModule ->
+ def nameCleansed = reactNativeModule["nameCleansed"]
+ appProject.dependencies {
+ // TODO(salakar): are other dependency scope methods such as `api` required?
+ implementation project(path: ":${nameCleansed}")
+ }
+ }
+ }
+
+ /**
+ * Code-gen a java file with all the detected ReactNativePackage instances automatically added
+ *
+ * @param outputDir
+ * @param generatedFileName
+ * @param generatedFileContentsTemplate
+ */
+ void generatePackagesFile(File outputDir, String generatedFileName, String generatedFileContentsTemplate, String packageName) {
+ ArrayList>[] packages = this.reactNativeModules
+
+ String packageImports = ""
+ String packageClassInstances = ""
+
+ if (packages.size() > 0) {
+ packageImports = "import ${packageName}.BuildConfig;\nimport ${packageName}.R;\n\n"
+ packageImports = packageImports + packages.collect {
+ "// ${it.name}\n${it.packageImportPath}"
+ }.join('\n')
+ packageClassInstances = ",\n " + packages.collect {
+ it.packageInstance
+ }.join(",\n ")
+ }
+
+ String generatedFileContents = generatedFileContentsTemplate
+ .replace("{{ packageImports }}", packageImports)
+ .replace("{{ packageClassInstances }}", packageClassInstances)
+
+ outputDir.mkdirs()
+ final FileTreeBuilder treeBuilder = new FileTreeBuilder(outputDir)
+ treeBuilder.file(generatedFileName).newWriter().withWriter { w ->
+ w << generatedFileContents
+ }
+ }
+
+ /**
+ * Runs a specified command using Runtime exec() in a specified directory.
+ * Throws when the command result is empty.
+ */
+ String getCommandOutput(String[] command) {
+ try {
+ def output = ""
+ def cmdProcess = Runtime.getRuntime().exec(command)
+ def bufferedReader = new BufferedReader(new InputStreamReader(cmdProcess.getInputStream()))
+ def buff = ""
+ def readBuffer = new StringBuffer()
+ while ((buff = bufferedReader.readLine()) != null) {
+ readBuffer.append(buff)
+ }
+ output = readBuffer.toString()
+ if (!output) {
+ this.logger.error("${LOG_PREFIX}Unexpected empty result of running '${command}' command.")
+ def bufferedErrorReader = new BufferedReader(new InputStreamReader(cmdProcess.getErrorStream()))
+ def errBuff = ""
+ def readErrorBuffer = new StringBuffer()
+ while ((errBuff = bufferedErrorReader.readLine()) != null) {
+ readErrorBuffer.append(errBuff)
+ }
+ throw new Exception(readErrorBuffer.toString())
+ }
+ return output
+ } catch (Exception exception) {
+ this.logger.error("${LOG_PREFIX}Running '${command}' command failed.")
+ throw exception
+ }
+ }
+
+ /**
+ * Runs a process to call the React Native CLI Config command and parses the output
+ */
+ ArrayList> getReactNativeConfig() {
+ if (this.reactNativeModules != null) return this.reactNativeModules
+
+ ArrayList> reactNativeModules = new ArrayList>()
+
+ /**
+ * Resolve the CLI location from Gradle file
+ *
+ * @todo: Sometimes Gradle can be called outside of the JavaScript hierarchy (-p flag) which
+ * will fail to resolve the script and the dependencies. We should resolve this soon.
+ *
+ * @todo: `fastlane` has been reported to not work too.
+ */
+
+ /**
+ * @todo: use commands below to resolve CLI path when upgrading to react-native v0.61.5
+ * def cliResolveScript = "console.log(require('react-native/cli').bin);"
+ * String[] nodeCommand = ["node", "-e", cliResolveScript]
+ * def cliPath = this.getCommandOutput(nodeCommand)
+ */
+
+ String[] reactNativeConfigCommand = ["node", REACT_NATIVE_CLI_BIN, "config"]
+ def reactNativeConfigOutput = this.getCommandOutput(reactNativeConfigCommand)
+
+ def json
+ try {
+ json = new JsonSlurper().parseText(reactNativeConfigOutput)
+ } catch (Exception exception) {
+ throw new Exception("Calling `${reactNativeConfigCommand}` finished with an exception. Error message: ${exception.toString()}. Output: ${reactNativeConfigOutput}");
+ }
+
+ json["dependencies"].each { name, value ->
+ def platformsConfig = value["platforms"];
+ def androidConfig = platformsConfig["android"]
+
+ if (androidConfig != null && androidConfig["sourceDir"] != null) {
+ this.logger.info("${LOG_PREFIX}Automatically adding native module '${name}'")
+
+ HashMap reactNativeModuleConfig = new HashMap()
+ reactNativeModuleConfig.put("name", name)
+ reactNativeModuleConfig.put("nameCleansed", name.replaceAll('[~*!\'()]+', '_').replaceAll('^@([\\w-.]+)/', '$1_'))
+ reactNativeModuleConfig.put("androidSourceDir", androidConfig["sourceDir"])
+ reactNativeModuleConfig.put("packageInstance", androidConfig["packageInstance"])
+ reactNativeModuleConfig.put("packageImportPath", androidConfig["packageImportPath"])
+ this.logger.trace("${LOG_PREFIX}'${name}': ${reactNativeModuleConfig.toMapString()}")
+
+ reactNativeModules.add(reactNativeModuleConfig)
+ } else {
+ this.logger.info("${LOG_PREFIX}Skipping native module '${name}'")
+ }
+ }
+
+ return reactNativeModules
+ }
+}
+
+/** -----------------------
+ * Exported Extensions
+ * ------------------------ */
+
+def autoModules = new ReactNativeModules(logger, jsAppDir)
+
+ext.applyNativeModulesSettingsGradle = { DefaultSettings defaultSettings, String root = null ->
+ if (root != null) {
+ logger.warn("${ReactNativeModules.LOG_PREFIX}Passing custom root is deprecated. CLI detects root automatically now.");
+ logger.warn("${ReactNativeModules.LOG_PREFIX}Please remove second argument to `applyNativeModulesSettingsGradle`.");
+ }
+ autoModules.addReactNativeModuleProjects(defaultSettings)
+}
+
+ext.applyNativeModulesAppBuildGradle = { Project project, String packageName, String root = null ->
+ if (root != null) {
+ logger.warn("${ReactNativeModules.LOG_PREFIX}Passing custom root is deprecated. CLI detects root automatically now");
+ logger.warn("${ReactNativeModules.LOG_PREFIX}Please remove second argument to `applyNativeModulesAppBuildGradle`.");
+ }
+ autoModules.addReactNativeModuleDependencies(project)
+
+ def generatedSrcDir = new File(buildDir, "generated/rncli/src/main/java")
+ def generatedCodeDir = new File(generatedSrcDir, generatedFilePackage.replace('.', '/'))
+
+ task generatePackageList {
+ doLast {
+ autoModules.generatePackagesFile(generatedCodeDir, generatedFileName, generatedFileContentsTemplate, packageName)
+ }
+ }
+
+ preBuild.dependsOn generatePackageList
+
+ android {
+ sourceSets {
+ main {
+ java {
+ srcDirs += generatedSrcDir
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/test-app-util.gradle b/android/test-app-util.gradle
new file mode 100644
index 000000000..00685b6f1
--- /dev/null
+++ b/android/test-app-util.gradle
@@ -0,0 +1,39 @@
+import java.nio.file.Paths
+
+/**
+ * Finds the path of the installed npm package with the given name using Node's
+ * module resolution algorithm, which searches "node_modules" directories up to
+ * the file system root. This handles various cases, including:
+ *
+ * - Working in the open-source RN repo:
+ * Gradle: /path/to/react-native/ReactAndroid
+ * Node module: /path/to/react-native/node_modules/[package]
+ *
+ * - Installing RN as a dependency of an app and searching for hoisted
+ * dependencies:
+ * Gradle: /path/to/app/node_modules/react-native/ReactAndroid
+ * Node module: /path/to/app/node_modules/[package]
+ *
+ * - Working in a larger repo (e.g., Facebook) that contains RN:
+ * Gradle: /path/to/repo/path/to/react-native/ReactAndroid
+ * Node module: /path/to/repo/node_modules/[package]
+ *
+ * The search begins at the given base directory (a File object). The returned
+ * path is a string.
+ */
+ext.findNodeModulesPath = { baseDir, packageName ->
+ def basePath = baseDir.toPath().normalize()
+
+ // Node's module resolution algorithm searches up to the root directory,
+ // after which the base path will be null
+ while (basePath) {
+ def candidatePath = Paths.get(basePath.toString(), "node_modules", packageName)
+ if (candidatePath.toFile().exists()) {
+ return candidatePath.toString()
+ }
+
+ basePath = basePath.getParent()
+ }
+
+ return null
+}
diff --git a/example/gradle.properties b/example/gradle.properties
new file mode 100644
index 000000000..c279d81bb
--- /dev/null
+++ b/example/gradle.properties
@@ -0,0 +1,9 @@
+# This properties are required to enable
+# AndroidX for the test-app.
+android.useAndroidX=true
+android.enableJetifier=true
+
+# A comma separated list of directories that are
+# expected to be copied to the test app.
+testApp.bundle=dist/main.jsbundle
+testApp.resources=dist/res
diff --git a/example/gradle/wrapper/gradle-wrapper.jar b/example/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..f6b961fd5
Binary files /dev/null and b/example/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/example/gradle/wrapper/gradle-wrapper.properties b/example/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..0fea76a3d
--- /dev/null
+++ b/example/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Mar 13 12:53:57 CET 2020
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
diff --git a/example/gradlew b/example/gradlew
new file mode 100755
index 000000000..cccdd3d51
--- /dev/null
+++ b/example/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/example/gradlew.bat b/example/gradlew.bat
new file mode 100644
index 000000000..f9553162f
--- /dev/null
+++ b/example/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/example/package.json b/example/package.json
index dba1b7a55..551403fbf 100644
--- a/example/package.json
+++ b/example/package.json
@@ -3,7 +3,8 @@
"version": "0.0.1",
"private": true,
"scripts": {
- "build:ios": "mkdirp dist && react-native bundle --entry-file index.js --platform ios --dev true --bundle-output dist/main.jsbundle --assets-dest dist --reset-cache"
+ "build:android": "mkdirp dist/res && react-native bundle --entry-file index.js --platform android --dev true --bundle-output dist/main.jsbundle --assets-dest dist/res --reset-cache",
+ "build:ios": "mkdirp dist && react-native bundle --entry-file index.js --platform ios --dev true --bundle-output dist/main.jsbundle --assets-dest dist --reset-cache"
},
"peerDependencies": {
"react": "^16.8.6",
diff --git a/example/settings.gradle b/example/settings.gradle
new file mode 100644
index 000000000..dcde47948
--- /dev/null
+++ b/example/settings.gradle
@@ -0,0 +1,4 @@
+rootProject.name='example'
+
+apply from: file("${rootDir}/node_modules/react-native-test-app/test-app.gradle")
+applyTestAppSettings(settings)
diff --git a/example/yarn.lock b/example/yarn.lock
index 35e78ed16..1877de9d0 100644
--- a/example/yarn.lock
+++ b/example/yarn.lock
@@ -3437,7 +3437,7 @@ react-is@^16.8.1, react-is@^16.8.4:
integrity sha512-GFMtL0vHkiBv9HluwNZTggSn/sCyEt9n02aM0dSAjGGyqyNlAyftYm4phPxdvCigG15JreC5biwxCgTAJZ7yAA==
react-native-test-app@../:
- version "0.0.4"
+ version "0.0.5"
dependencies:
"@babel/core" "^7.0.0"
"@babel/runtime" "^7.0.0"
diff --git a/package.json b/package.json
index b6d42f300..c6a275682 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,7 @@
"type": "git",
"url": "https://github.com/microsoft/react-native-test-app.git"
},
- "version": "0.0.4",
+ "version": "0.0.5",
"description": "react-native-test-app provides a test app for all supported platforms as a package",
"homepage": "https://github.com/microsoft/react-native-test-app",
"scripts": {
diff --git a/test-app-util.gradle b/test-app-util.gradle
deleted file mode 100644
index c8cf4e041..000000000
--- a/test-app-util.gradle
+++ /dev/null
@@ -1,39 +0,0 @@
-import java.nio.file.Paths
-
-/**
-* Finds the path of the installed npm package with the given name using Node's
-* module resolution algorithm, which searches "node_modules" directories up to
-* the file system root. This handles various cases, including:
-*
-* - Working in the open-source RN repo:
-* Gradle: /path/to/react-native/ReactAndroid
-* Node module: /path/to/react-native/node_modules/[package]
-*
-* - Installing RN as a dependency of an app and searching for hoisted
-* dependencies:
-* Gradle: /path/to/app/node_modules/react-native/ReactAndroid
-* Node module: /path/to/app/node_modules/[package]
-*
-* - Working in a larger repo (e.g., Facebook) that contains RN:
-* Gradle: /path/to/repo/path/to/react-native/ReactAndroid
-* Node module: /path/to/repo/node_modules/[package]
-*
-* The search begins at the given base directory (a File object). The returned
-* path is a string.
-*/
-ext.findNodeModulePath = { packageName ->
- def basePath = rootDir.toPath().normalize()
-
- // Node's module resolution algorithm searches up to the root directory,
- // after which the base path will be null
- while (basePath) {
- def candidatePath = Paths.get(basePath.toString(), "node_modules", packageName)
- if (candidatePath.toFile().exists()) {
- return candidatePath.toString()
- }
-
- basePath = basePath.getParent()
- }
-
- return null
-}
diff --git a/test-app.gradle b/test-app.gradle
index 1a10ee7d8..de9fe9677 100644
--- a/test-app.gradle
+++ b/test-app.gradle
@@ -1,176 +1,56 @@
import org.gradle.initialization.DefaultSettings
-def generatedFileName = "TestAppPackageList.java"
-def generatedFilePackage = "com.facebook.react"
-def generatedFileContentsTemplate = """
-package $generatedFilePackage;
+private static void apply(Settings settings) {
+ def projectDir = settings.findNodeModulesPath(settings.rootDir, "react-native-test-app")
-import com.facebook.react.ReactPackage;
-import java.util.Arrays;
-import java.util.ArrayList;
-
-{{ packageImports }}
-
-public class TestAppPackageList {
- public ArrayList getPackages() {
- return new ArrayList<>(Arrays.asList(
- {{ packageClassInstances }}
- ));
- }
+ settings.include(":app")
+ settings.project(":app")
+ .projectDir = new File("${projectDir}/android/app")
}
-"""
-
-class TestAppPlugin {
- private String generatedFileName;
- private String generatedFilePackage;
- private String generatedFileContentsTemplate;
-
- private String packageName;
- private ArrayList> packages = new ArrayList<>();
-
- TestAppPlugin(
- String generatedFileName,
- String generatedFilePackage,
- String generatedFileContentsTemplate
- ) {
- this.generatedFileName = generatedFileName;
- this.generatedFilePackage = generatedFilePackage;
- this.generatedFileContentsTemplate = generatedFileContentsTemplate;
- }
-
- void apply(Settings settings) {
- def projectDir = settings.findNodeModulePath(
- "react-native-test-app")
-
- settings.include(":app")
- settings.project(":app")
- .projectDir = new File("${projectDir}/android/app")
- }
-
- void apply(Project project) {
- def appProject = project.project(":app")
-
- // TODO: support configurations other than implementation
- def appDependencies = appProject.getConfigurations()
- .getByName("implementation")
- .getDependencies()
-
- def dependency = appProject.getDependencies()
- .create(project)
-
- appDependencies.add(dependency)
-
- packageName = getPackageName(project)
- packages = getPackages(project)
- }
-
- private String getPackageName(Project project) {
- def projectDir = project.projectDir.absolutePath
-
- def files = new FileNameFinder()
- .getFileNames(projectDir, "**/AndroidManifest.xml",
- "node_modules/** **/build/** **/debug/** Examples/** examples/**")
- def manifestFile = files.first()
- def manifest = (new XmlParser()).parse(manifestFile)
-
- return manifest.attribute("package")
- }
- private ArrayList> getPackages(Project project) {
- def projectDir = project.projectDir.absolutePath
+def scriptDir = buildscript.sourceFile.getParent()
- def javaFiles = new FileNameFinder()
- .getFileNames(projectDir, "**/*.java")
- def kotlinFiles = new FileNameFinder()
- .getFileNames(projectDir, "**/*.kt")
-
- def sourceFiles = new ArrayList<>()
- sourceFiles.addAll(kotlinFiles)
- sourceFiles.addAll(javaFiles)
+apply from: "$scriptDir/android/test-app-util.gradle"
+apply from: "$scriptDir/android/test-app-native-modules.gradle"
- def pattern = ~/class\s+(\w+[^(\s]*)[\s\w():]*(\s+implements\s+|:)[\s\w():,]*[^{]*ReactPackage/
-
- ArrayList> packages = new ArrayList<>();
-
- sourceFiles.forEach { path ->
- def matcher = pattern.matcher(new File(path).text)
- if (matcher.find()) {
- def packageClassName = matcher.group(1);
- def packageImportPath = "import ${packageName}.${packageClassName};"
- def packageInstance = "new ${packageClassName}()"
-
- HashMap config = new HashMap()
- config.put("packageInstance", packageInstance)
- config.put("packageImportPath", packageImportPath)
-
- packages.add(config);
- }
- }
+ext.applyTestAppSettings = { DefaultSettings defaultSettings ->
+ apply(defaultSettings)
+ applyNativeModulesSettingsGradle(defaultSettings)
+}
- return packages
- }
+ext.applyTestAppModule = { Project project, String packageName ->
+ applyNativeModulesAppBuildGradle(project, packageName)
- /**
- * Code-gen a java file with all the detected ReactNativePackage instances automatically added
- *
- * @param outputDir
- */
- void generatePackagesFile(File outputDir) {
- String packageImports = ""
- String packageClassInstances = ""
+ def generatedAssetsDir = file("${buildDir}/generated/rncli/src/main/assets/")
+ generatedAssetsDir.mkdirs()
- if (packages.size() > 0) {
- packageImports = "import ${packageName}.BuildConfig;\nimport ${packageName}.R;\n\n"
- packageImports = packageImports + packages.collect { "${it.packageImportPath}" }.join('\n')
- packageClassInstances = packages.collect { it.packageInstance }.join(",\n ")
- }
+ def generatedResDir = file("${buildDir}/generated/rncli/src/main/res/")
+ generatedResDir.mkdirs()
- String generatedFileContents = generatedFileContentsTemplate
- .replace("{{ packageImports }}", packageImports)
- .replace("{{ packageClassInstances }}", packageClassInstances)
+ task copyBundle(type: Copy) {
+ def bundlePath = project.getProperties()
+ .get("testApp.bundle")
- outputDir.mkdirs()
- final FileTreeBuilder treeBuilder = new FileTreeBuilder(outputDir)
- treeBuilder.file(generatedFileName).newWriter().withWriter { w ->
- w << generatedFileContents
- }
+ from "${rootDir}/${bundlePath}"
+ into generatedAssetsDir
}
-}
-
-def buildscriptDir = buildscript.sourceFile.getParent()
-apply from: "$buildscriptDir/test-app-util.gradle"
-def plugin = new TestAppPlugin(
- generatedFileName,
- generatedFilePackage,
- generatedFileContentsTemplate
-)
-
-ext.applyTestAppSettings = { DefaultSettings defaultSettings ->
- plugin.apply(defaultSettings)
-}
+ task copyResources(type: Copy) {
+ def resourcesPath = project.getProperties()
+ .get("testApp.resources")
-// TODO: make codegen smarter to merge PackageList and TestAppPackageList files
-ext.applyTestAppModule = { Project project ->
- plugin.apply(project)
-
- def generatedSrcDir = new File(buildDir, "generated/rncli/src/main/java")
- def generatedCodeDir = new File(generatedSrcDir, generatedFilePackage.replace('.', '/'))
-
- task generateTestAppPackageList {
- doLast {
- plugin.generatePackagesFile(generatedCodeDir)
- }
+ from "${rootDir}/${resourcesPath}"
+ into generatedResDir
}
- preBuild.dependsOn generateTestAppPackageList
+ preBuild.dependsOn(copyBundle)
+ preBuild.dependsOn(copyResources)
android {
sourceSets {
main {
- java {
- srcDirs += generatedSrcDir
- }
+ assets.srcDirs += generatedAssetsDir
+ res.srcDirs += generatedResDir
}
}
}