-
- com.example.ADP_Github
+ io.github.rfazi.androiddynamicmodules
+ Android Dynamic Modules
+ Ruben Fazi
-
- ADP_Github
-
-
- YourCompany
-
-
+
+ Enable or disable Gradle modules from the IDE and keep Android dynamic feature declarations
+ aligned with the selected modules.
+
+
+ The plugin supports Gradle Groovy and Kotlin DSL projects, validates all edits before writing,
+ applies changes as one IDE command, and starts a Gradle sync only when files changed.
+
+ ]]>
-
com.intellij.modules.platform
+ org.jetbrains.plugins.gradle
-
-
-
-
-
+
-
\ No newline at end of file
+
diff --git a/src/test/kotlin/io/github/rfazi/androiddynamicmodules/core/DynamicFeaturesDocumentTest.kt b/src/test/kotlin/io/github/rfazi/androiddynamicmodules/core/DynamicFeaturesDocumentTest.kt
new file mode 100644
index 0000000..06d9472
--- /dev/null
+++ b/src/test/kotlin/io/github/rfazi/androiddynamicmodules/core/DynamicFeaturesDocumentTest.kt
@@ -0,0 +1,68 @@
+package io.github.rfazi.androiddynamicmodules.core
+
+import java.nio.file.Path
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+
+class DynamicFeaturesDocumentTest {
+ @Test
+ fun `rewrites Groovy dynamic features`() {
+ val source = """
+ android {
+ dynamicFeatures = [":df_search", ":df_payments"]
+ }
+ """.trimIndent() + "\n"
+ val document = assertNotNull(DynamicFeaturesDocument.parse(Path.of("build.gradle"), source))
+
+ val rendered = document.render(listOf(":df_payments"))
+
+ assertEquals(
+ """
+ android {
+ dynamicFeatures = [":df_payments"]
+ }
+ """.trimIndent() + "\n",
+ rendered,
+ )
+ }
+
+ @Test
+ fun `consolidates Kotlin dynamic feature declarations`() {
+ val source = """
+ android {
+ dynamicFeatures += setOf(":df_search")
+ dynamicFeatures += setOf(
+ ":df_payments",
+ )
+ }
+ """.trimIndent() + "\n"
+ val document = assertNotNull(DynamicFeaturesDocument.parse(Path.of("build.gradle.kts"), source))
+
+ val rendered = document.render(listOf(":df_payments", ":df_search"))
+
+ assertEquals(
+ """
+ android {
+ dynamicFeatures.clear()
+ dynamicFeatures += setOf(":df_payments", ":df_search")
+ }
+ """.trimIndent() + "\n",
+ rendered,
+ )
+ }
+
+ @Test
+ fun `keeps an empty Kotlin declaration editable on the next run`() {
+ val source = "android {\n dynamicFeatures += setOf(\":df_search\")\n}\n"
+ val firstPass = assertNotNull(DynamicFeaturesDocument.parse(Path.of("build.gradle.kts"), source))
+ .render(emptyList())
+ val secondPass = assertNotNull(DynamicFeaturesDocument.parse(Path.of("build.gradle.kts"), firstPass))
+ .render(listOf(":df_search"))
+
+ assertEquals(
+ "android {\n dynamicFeatures.clear()\n dynamicFeatures += setOf(\":df_search\")\n}\n",
+ secondPass,
+ )
+ }
+}
diff --git a/src/test/kotlin/io/github/rfazi/androiddynamicmodules/core/GradleSettingsDocumentTest.kt b/src/test/kotlin/io/github/rfazi/androiddynamicmodules/core/GradleSettingsDocumentTest.kt
new file mode 100644
index 0000000..d75832f
--- /dev/null
+++ b/src/test/kotlin/io/github/rfazi/androiddynamicmodules/core/GradleSettingsDocumentTest.kt
@@ -0,0 +1,65 @@
+package io.github.rfazi.androiddynamicmodules.core
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+class GradleSettingsDocumentTest {
+ @Test
+ fun `parses Kotlin and Groovy include forms without matching includeBuild`() {
+ val source = """
+ pluginManagement { includeBuild("build-logic") }
+ include(":app", ":feature", ":feature-auth")
+ // include ':optional' // local-only
+ include ":legacy"
+ """.trimIndent()
+
+ val document = GradleSettingsDocument.parse(source)
+ val modules = document.modules.associate { it.path to it.enabled }
+
+ assertEquals(setOf(":app", ":feature", ":feature-auth", ":optional", ":legacy"), modules.keys)
+ assertTrue(modules.getValue(":app"))
+ assertFalse(modules.getValue(":optional"))
+ }
+
+ @Test
+ fun `renders each module independently and preserves trailing comments`() {
+ val source = """
+ include(":app", ":feature", ":feature-auth")
+ // include(":optional") // local-only
+ includeBuild("build-logic")
+ """.trimIndent() + "\n"
+ val document = GradleSettingsDocument.parse(source)
+
+ val rendered = document.render(
+ mapOf(
+ ":app" to true,
+ ":feature" to false,
+ ":feature-auth" to true,
+ ":optional" to true,
+ ),
+ )
+
+ assertEquals(
+ """
+ include(":app")
+ // include(":feature")
+ include(":feature-auth")
+ include(":optional") // local-only
+ includeBuild("build-logic")
+ """.trimIndent() + "\n",
+ rendered,
+ )
+ }
+
+ @Test
+ fun `supports multiline includes and CRLF line endings`() {
+ val source = "include(\r\n \":app\",\r\n \":feature:login\",\r\n)\r\n"
+ val document = GradleSettingsDocument.parse(source)
+
+ val rendered = document.render(mapOf(":app" to true, ":feature:login" to false))
+
+ assertEquals("include(\":app\")\r\n// include(\":feature:login\")\r\n", rendered)
+ }
+}
diff --git a/src/test/kotlin/io/github/rfazi/androiddynamicmodules/core/ModuleProjectServiceTest.kt b/src/test/kotlin/io/github/rfazi/androiddynamicmodules/core/ModuleProjectServiceTest.kt
new file mode 100644
index 0000000..67412ca
--- /dev/null
+++ b/src/test/kotlin/io/github/rfazi/androiddynamicmodules/core/ModuleProjectServiceTest.kt
@@ -0,0 +1,70 @@
+package io.github.rfazi.androiddynamicmodules.core
+
+import org.junit.Rule
+import org.junit.rules.TemporaryFolder
+import java.nio.file.Files
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertTrue
+
+class ModuleProjectServiceTest {
+ @get:Rule
+ val temporaryFolder = TemporaryFolder()
+
+ @Test
+ fun `plans settings and Kotlin dynamic feature changes without touching disk`() {
+ val root = temporaryFolder.newFolder("project").toPath()
+ val settings = root.resolve("settings.gradle.kts")
+ val appBuild = root.resolve("app/build.gradle.kts")
+ Files.createDirectories(appBuild.parent)
+ Files.writeString(settings, "include(\":app\", \":optional\", \":df_search\")\n")
+ Files.writeString(
+ appBuild,
+ "android {\n dynamicFeatures += setOf(\":df_search\")\n}\n",
+ )
+
+ val snapshot = ModuleProjectService.load(root)
+ val plan = ModuleProjectService.plan(
+ snapshot,
+ mapOf(":app" to false, ":optional" to false, ":df_search" to false),
+ )
+
+ assertEquals(2, plan.changes.size)
+ assertTrue(plan.changes.first { it.path == settings }.updated.contains("include(\":app\")"))
+ assertTrue(plan.changes.first { it.path == settings }.updated.contains("// include(\":optional\")"))
+ assertTrue(plan.changes.first { it.path == appBuild }.updated.contains("dynamicFeatures.clear()"))
+ assertEquals("include(\":app\", \":optional\", \":df_search\")\n", Files.readString(settings))
+ }
+
+ @Test
+ fun `fails closed when dynamic features exist without an application build file`() {
+ val root = temporaryFolder.newFolder("missing-app").toPath()
+ val settings = root.resolve("settings.gradle.kts")
+ Files.writeString(settings, "include(\":app\", \":df_search\")\n")
+ val snapshot = ModuleProjectService.load(root)
+
+ assertFailsWith