diff --git a/app/build.gradle.kts b/app/build.gradle.kts index effa886c4d..50db9aa617 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -203,7 +203,7 @@ knit { dependencies { allLibrariesImpl() allServicesImpl() - allFeaturesImpl() + allFeaturesImpl(rootDir) implementation(projects.tests.uitests) implementation(projects.anvilannotations) implementation(projects.appnav) diff --git a/appnav/build.gradle.kts b/appnav/build.gradle.kts index 082d876fbd..1c1c1635b4 100644 --- a/appnav/build.gradle.kts +++ b/appnav/build.gradle.kts @@ -38,7 +38,7 @@ dependencies { implementation(libs.dagger) kapt(libs.dagger.compiler) - allFeaturesApi() + allFeaturesApi(rootDir) implementation(projects.libraries.core) implementation(projects.libraries.architecture) diff --git a/docs/_developer_onboarding.md b/docs/_developer_onboarding.md index a526e0de33..9b07f37daf 100644 --- a/docs/_developer_onboarding.md +++ b/docs/_developer_onboarding.md @@ -269,10 +269,42 @@ Here are the main points: #### Template and naming -There is a template module to easily start a new feature. When creating a new module, you can just copy paste the template. It is -located [here](../features/template). - -For the naming rules, please follow what is being currently used in the template module. +This documentation provides you with the steps to install and use the AS plugin for generating modules in your project. +The plugin and templates will help you quickly create new features with a standardized structure. + +A. Installation + +Follow these steps to install and configure the plugin and templates: + +1. Install the AS plugin for generating modules : + [Generate Module from Template](https://plugins.jetbrains.com/plugin/13586-generate-module-from-template) +2. Import file templates in AS : + - Navigate to File/Manage IDE Settings/Import Settings + - Pick the `tools/templates/file_templates.zip` files + - Click on OK +3. Configure generate-module-from-template plugin : + - Navigate to AS/Settings/Tools/Module Template Settings + - Click on + / Import From File + - Pick the `tools/templates/FeatureModule.json` + +Everything should be ready to use. + +B. Usage + +Example for a new feature called RoomDetails: + +1. Right-click on the features package and click on Create Module from Template +2. Fill the 2 text fields like so: + - MODULE_NAME = roomdetails + - FEATURE_NAME = RoomDetails +3. Click on Next +4. Verify that the structure looks ok and click on Finish +5. The modules api/impl should be created under `features/roomdetails` directory. +6. Sync project with Gradle so the modules are recognized (no need to add them to settings.gradle). +7. You can now add more Presentation classes (Events, State, StateProvider, View, Presenter) in the impl module with the `Template Presentation Classes`. + To use it, just right click on the package where you want to generate classes, and click on `Template Presentation Classes`. + Fill the text field with the base name of the classes, ie `RootRoomDetails` in the `root` package. + Note that naming of files and classes is important, since those names are used to set up code coverage rules. For instance, presenters MUST have a suffix `Presenter`,states MUST have a suffix `State`, etc. Also we want to have a common naming along all the modules. diff --git a/features/template/.gitignore b/features/template/.gitignore deleted file mode 100644 index 42afabfd2a..0000000000 --- a/features/template/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/features/template/build.gradle.kts b/features/template/build.gradle.kts deleted file mode 100644 index 409a0c9bc5..0000000000 --- a/features/template/build.gradle.kts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (c) 2023 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed -@Suppress("DSL_SCOPE_VIOLATION") -plugins { - id("io.element.android-compose-library") - alias(libs.plugins.anvil) - alias(libs.plugins.ksp) -} - -android { - // TODO change the namespace (and your classes package) - namespace = "io.element.android.features.template" -} - -anvil { - generateDaggerFactories.set(true) -} - -dependencies { - anvil(projects.anvilcodegen) - implementation(projects.anvilannotations) - - implementation(projects.libraries.core) - implementation(projects.libraries.architecture) - implementation(projects.libraries.matrix.api) - implementation(projects.libraries.matrixui) - implementation(projects.libraries.designsystem) - implementation(projects.libraries.elementresources) - implementation(projects.libraries.uiStrings) - - testImplementation(libs.test.junit) - testImplementation(libs.coroutines.test) - testImplementation(libs.molecule.runtime) - testImplementation(libs.test.truth) - testImplementation(libs.test.turbine) - testImplementation(projects.libraries.matrix.test) - - androidTestImplementation(libs.test.junitext) - - ksp(libs.showkase.processor) -} diff --git a/features/template/consumer-rules.pro b/features/template/consumer-rules.pro deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/features/template/proguard-rules.pro b/features/template/proguard-rules.pro deleted file mode 100644 index 481bb43481..0000000000 --- a/features/template/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/features/template/src/main/AndroidManifest.xml b/features/template/src/main/AndroidManifest.xml deleted file mode 100644 index 86d497f107..0000000000 --- a/features/template/src/main/AndroidManifest.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - diff --git a/features/template/src/main/kotlin/io/element/android/features/template/TemplateEvents.kt b/features/template/src/main/kotlin/io/element/android/features/template/TemplateEvents.kt deleted file mode 100644 index 57d0b0cc25..0000000000 --- a/features/template/src/main/kotlin/io/element/android/features/template/TemplateEvents.kt +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright (c) 2023 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.features.template - -// TODO Add your events or remove the file completely if no events -sealed interface TemplateEvents { - object MyEvent: TemplateEvents -} diff --git a/features/template/src/main/kotlin/io/element/android/features/template/TemplateNode.kt b/features/template/src/main/kotlin/io/element/android/features/template/TemplateNode.kt deleted file mode 100644 index 4be5178ede..0000000000 --- a/features/template/src/main/kotlin/io/element/android/features/template/TemplateNode.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (c) 2023 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.features.template - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import com.bumble.appyx.core.modality.BuildContext -import com.bumble.appyx.core.node.Node -import com.bumble.appyx.core.plugin.Plugin -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import io.element.android.anvilannotations.ContributesNode -import io.element.android.libraries.di.AppScope - -// TODO Change to use the right Scope for your feature. For now it can be AppScope, SessionScope or RoomScope -@ContributesNode(AppScope::class) -class TemplateNode @AssistedInject constructor( - @Assisted buildContext: BuildContext, - @Assisted plugins: List, - private val presenter: TemplatePresenter, -) : Node(buildContext, plugins = plugins) { - - @Composable - override fun View(modifier: Modifier) { - val state = presenter.present() - TemplateView( - state = state, - modifier = modifier - ) - } -} diff --git a/features/template/src/main/kotlin/io/element/android/features/template/TemplatePresenter.kt b/features/template/src/main/kotlin/io/element/android/features/template/TemplatePresenter.kt deleted file mode 100644 index 254a5fc048..0000000000 --- a/features/template/src/main/kotlin/io/element/android/features/template/TemplatePresenter.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 2023 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.features.template - -import androidx.compose.runtime.Composable -import io.element.android.libraries.architecture.Presenter -import javax.inject.Inject - -class TemplatePresenter @Inject constructor() : Presenter { - - @Composable - override fun present(): TemplateState { - - fun handleEvents(event: TemplateEvents) { - when (event) { - TemplateEvents.MyEvent -> Unit - } - } - - return TemplateState( - eventSink = ::handleEvents - ) - } -} diff --git a/features/template/src/main/kotlin/io/element/android/features/template/TemplateState.kt b/features/template/src/main/kotlin/io/element/android/features/template/TemplateState.kt deleted file mode 100644 index b9a48a7378..0000000000 --- a/features/template/src/main/kotlin/io/element/android/features/template/TemplateState.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (c) 2023 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.features.template - -// TODO add your ui models. Remove the eventSink if you don't have events. -// Do not use default value, so no member get forgotten in the presenters. -data class TemplateState( - val eventSink: (TemplateEvents) -> Unit -) diff --git a/features/template/src/main/kotlin/io/element/android/features/template/TemplateStateProvider.kt b/features/template/src/main/kotlin/io/element/android/features/template/TemplateStateProvider.kt deleted file mode 100644 index 7541e1667a..0000000000 --- a/features/template/src/main/kotlin/io/element/android/features/template/TemplateStateProvider.kt +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (c) 2023 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.features.template - -import androidx.compose.ui.tooling.preview.PreviewParameterProvider - -open class TemplateStateProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - aTemplateState(), - // Add other state here - ) -} - -fun aTemplateState() = TemplateState( - eventSink = {} -) diff --git a/features/template/src/main/kotlin/io/element/android/features/template/TemplateView.kt b/features/template/src/main/kotlin/io/element/android/features/template/TemplateView.kt deleted file mode 100644 index c7456ad49a..0000000000 --- a/features/template/src/main/kotlin/io/element/android/features/template/TemplateView.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (c) 2022 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.features.template - -import androidx.compose.foundation.layout.Box -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import io.element.android.libraries.designsystem.preview.ElementPreviewDark -import io.element.android.libraries.designsystem.preview.ElementPreviewLight -import io.element.android.libraries.designsystem.theme.components.Text - -@Composable -fun TemplateView( - state: TemplateState, - modifier: Modifier = Modifier, -) { - Box(modifier, contentAlignment = Alignment.Center) { - Text( - "Template feature view", - color = MaterialTheme.colorScheme.primary, - ) - } -} - -@Preview -@Composable -fun TemplateViewLightPreview(@PreviewParameter(TemplateStateProvider::class) state: TemplateState) = - ElementPreviewLight { ContentToPreview(state) } - -@Preview -@Composable -fun TemplateViewDarkPreview(@PreviewParameter(TemplateStateProvider::class) state: TemplateState) = - ElementPreviewDark { ContentToPreview(state) } - -@Composable -private fun ContentToPreview(state: TemplateState) { - TemplateView( - state = state, - ) -} diff --git a/features/template/src/test/kotlin/io/element/android/features/template/TemplatePresenterTests.kt b/features/template/src/test/kotlin/io/element/android/features/template/TemplatePresenterTests.kt deleted file mode 100644 index a14cd2761e..0000000000 --- a/features/template/src/test/kotlin/io/element/android/features/template/TemplatePresenterTests.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (c) 2023 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -@file:OptIn(ExperimentalCoroutinesApi::class) - -package io.element.android.features.template - -import app.cash.molecule.RecompositionClock -import app.cash.molecule.moleculeFlow -import app.cash.turbine.test -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runTest -import org.junit.Test - -class TemplatePresenterTests { - - @Test - fun `present - initial state`() = runTest { - val presenter = TemplatePresenter() - moleculeFlow(RecompositionClock.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - assertThat(initialState) - } - } - - @Test - fun `present - send event`() = runTest { - val presenter = TemplatePresenter() - moleculeFlow(RecompositionClock.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - initialState.eventSink.invoke(TemplateEvents.MyEvent) - } - } -} diff --git a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt index 658b6bafa8..314421ebc8 100644 --- a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt +++ b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt @@ -22,6 +22,7 @@ import gradle.kotlin.dsl.accessors._71f190358cebd46a469f2989484fd643.implementat import org.gradle.accessors.dm.LibrariesForLibs import org.gradle.kotlin.dsl.DependencyHandlerScope import org.gradle.kotlin.dsl.project +import java.io.File /** * Dependencies used by all the modules @@ -51,6 +52,21 @@ fun DependencyHandlerScope.composeDependencies(libs: LibrariesForLibs) { implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.5") } +private fun DependencyHandlerScope.addImplementationProjects(directory: File, path: String, nameFilter: String) { + directory.listFiles().orEmpty().forEach { file -> + if (file.isDirectory) { + val newPath = "$path:${file.name}" + val buildFile = File(file, "build.gradle.kts") + if (buildFile.exists() && file.name == nameFilter) { + implementation(project(newPath)) + println("Added implementation(project($newPath))") + } else { + addImplementationProjects(file, newPath, nameFilter) + } + } + } +} + fun DependencyHandlerScope.allLibrariesImpl() { implementation(project(":libraries:designsystem")) implementation(project(":libraries:matrix:impl")) @@ -71,28 +87,11 @@ fun DependencyHandlerScope.allServicesImpl() { implementation(project(":services:toolbox:impl")) } -fun DependencyHandlerScope.allFeaturesApi() { - implementation(project(":features:onboarding:api")) - implementation(project(":features:login:api")) - implementation(project(":features:logout:api")) - implementation(project(":features:roomlist:api")) - implementation(project(":features:messages:api")) - implementation(project(":features:rageshake:api")) - implementation(project(":features:preferences:api")) - implementation(project(":features:createroom:api")) - implementation(project(":features:verifysession:api")) - implementation(project(":features:selectusers:api")) +fun DependencyHandlerScope.allFeaturesApi(rootDir: File) { + val featuresDir = File(rootDir, "features") + addImplementationProjects(featuresDir, ":features", "api") } - -fun DependencyHandlerScope.allFeaturesImpl() { - implementation(project(":features:onboarding:impl")) - implementation(project(":features:login:impl")) - implementation(project(":features:logout:impl")) - implementation(project(":features:roomlist:impl")) - implementation(project(":features:messages:impl")) - implementation(project(":features:rageshake:impl")) - implementation(project(":features:preferences:impl")) - implementation(project(":features:createroom:impl")) - implementation(project(":features:verifysession:impl")) - implementation(project(":features:selectusers:impl")) +fun DependencyHandlerScope.allFeaturesImpl(rootDir: File) { + val featuresDir = File(rootDir, "features") + addImplementationProjects(featuresDir, ":features", "impl") } diff --git a/settings.gradle.kts b/settings.gradle.kts index 8bb533a394..2a491a2743 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -58,7 +58,6 @@ include(":tests:uitests") include(":anvilannotations") include(":anvilcodegen") include(":libraries:architecture") -include(":features:template") include(":libraries:androidutils") include(":samples:minimal") include(":libraries:encrypted-db") @@ -74,24 +73,20 @@ include(":services:appnavstate:impl") include(":services:toolbox:api") include(":services:toolbox:impl") -include(":features:onboarding:api") -include(":features:onboarding:impl") -include(":features:logout:api") -include(":features:logout:impl") -include(":features:roomlist:api") -include(":features:roomlist:impl") -include(":features:rageshake:api") -include(":features:rageshake:impl") -include(":features:rageshake:test") -include(":features:preferences:api") -include(":features:preferences:impl") -include(":features:messages:api") -include(":features:messages:impl") -include(":features:login:api") -include(":features:login:impl") -include(":features:createroom:api") -include(":features:createroom:impl") -include(":features:verifysession:api") -include(":features:verifysession:impl") -include(":features:selectusers:api") -include(":features:selectusers:impl") +fun includeProjects(directory: File, path: String) { + directory.listFiles().orEmpty().forEach { file -> + if (file.isDirectory) { + val newPath = "$path:${file.name}" + val buildFile = File(file, "build.gradle.kts") + if (buildFile.exists()) { + include(newPath) + println("Included project: $newPath") + } else { + includeProjects(file, newPath) + } + } + } +} + +val featuresDir = File(rootDir, "features") +includeProjects(featuresDir, ":features") diff --git a/tests/uitests/build.gradle.kts b/tests/uitests/build.gradle.kts index b7abf23940..9c667beae0 100644 --- a/tests/uitests/build.gradle.kts +++ b/tests/uitests/build.gradle.kts @@ -40,5 +40,5 @@ dependencies { ksp(libs.showkase.processor) allLibrariesImpl() - allFeaturesImpl() + allFeaturesImpl(rootDir) } diff --git a/tools/templates/FeatureModule.json b/tools/templates/FeatureModule.json new file mode 100644 index 0000000000..4ad5e3a676 --- /dev/null +++ b/tools/templates/FeatureModule.json @@ -0,0 +1 @@ +{"template":{"name":"","isDir":true,"placeholders":{"MODULE_NAME":"","FEATURE_NAME":"","BUILD_GRADLE_API":"build.gradle.kts","BUILD_GRADLE_IMPL":"build.gradle.kts"},"fileTemplates":{"${FEATURE_NAME}EntryPoint":"Template Module Feature Entry Point API","Default${FEATURE_NAME}EntryPoint":"Template Module Feature Entry Point Flow Impl","${BUILD_GRADLE_API}":"Template Module Feature Build Gradle API","${BUILD_GRADLE_IMPL}":"Template Module Feature Build Gradle Impl","${FEATURE_NAME}FlowNode":"Template Module Feature Node Flow Impl"},"realChildren":[{"name":"${MODULE_NAME}","isDir":true,"realChildren":[{"name":"api","isDir":true,"realChildren":[{"name":"src","isDir":true,"realChildren":[{"name":"main","isDir":true,"realChildren":[{"name":"kotlin","isDir":true,"realChildren":[{"name":"io","isDir":true,"realChildren":[{"name":"element","isDir":true,"realChildren":[{"name":"android","isDir":true,"realChildren":[{"name":"features","isDir":true,"realChildren":[{"name":"${MODULE_NAME}","isDir":true,"realChildren":[{"name":"api","isDir":true,"realChildren":[{"name":"${FEATURE_NAME}EntryPoint","isDir":false,"placeholders":{},"fileTemplates":{},"realChildren":[]}]}]}]}]}]}]}]}]}]},{"name":"${BUILD_GRADLE_API}","isDir":false,"placeholders":{},"fileTemplates":{},"realChildren":[]}]},{"name":"impl","isDir":true,"realChildren":[{"name":"src","isDir":true,"realChildren":[{"name":"main","isDir":true,"realChildren":[{"name":"kotlin","isDir":true,"realChildren":[{"name":"io","isDir":true,"realChildren":[{"name":"element","isDir":true,"realChildren":[{"name":"android","isDir":true,"realChildren":[{"name":"features","isDir":true,"realChildren":[{"name":"${MODULE_NAME}","isDir":true,"realChildren":[{"name":"impl","isDir":true,"realChildren":[{"name":"Default${FEATURE_NAME}EntryPoint","isDir":false,"placeholders":{},"fileTemplates":{},"realChildren":[]},{"name":"${FEATURE_NAME}FlowNode","isDir":false,"placeholders":{},"fileTemplates":{},"realChildren":[]}]}]}]}]}]}]}]}]},{"name":"test","isDir":true,"realChildren":[{"name":"kotlin","isDir":true,"realChildren":[{"name":"io","isDir":true,"realChildren":[{"name":"element","isDir":true,"realChildren":[{"name":"android","isDir":true,"realChildren":[{"name":"features","isDir":true,"realChildren":[{"name":"${MODULE_NAME}","isDir":true,"realChildren":[{"name":"impl","isDir":true,"realChildren":[]}]}]}]}]}]}]}]}]},{"name":"${BUILD_GRADLE_IMPL}","isDir":false,"placeholders":{},"fileTemplates":{},"realChildren":[]}]}]}]},"language":"java","templateName":"FeatureModule","lowercaseDir":true,"capitalizeFile":false,"packageNameToDir":false} \ No newline at end of file diff --git a/tools/templates/file_templates.zip b/tools/templates/file_templates.zip new file mode 100644 index 0000000000..7352ac3074 Binary files /dev/null and b/tools/templates/file_templates.zip differ