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 } } }