From 0051bd2cf3a21c56c4a8658ec4c321acf9d9be53 Mon Sep 17 00:00:00 2001 From: Inaki Villar Date: Fri, 28 Aug 2026 11:48:08 -0700 Subject: [PATCH] fix: refactor generated module paths to a shared layout helper --- .../generator/GeneratedModuleLayout.kt | 117 +++++++++++++++++ .../classes/ClassGeneratorAndroid.kt | 13 +- .../generator/resources/ResourceGenerator.kt | 21 +--- .../generator/test/TestGeneratorAndroid.kt | 14 +-- .../projectgenerator/writer/ModulesWriter.kt | 18 +-- .../generator/GeneratedModuleLayoutTest.kt | 119 ++++++++++++++++++ 6 files changed, 261 insertions(+), 41 deletions(-) create mode 100644 project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/GeneratedModuleLayout.kt create mode 100644 project-generator/src/test/kotlin/io/github/cdsap/projectgenerator/generator/GeneratedModuleLayoutTest.kt diff --git a/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/GeneratedModuleLayout.kt b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/GeneratedModuleLayout.kt new file mode 100644 index 00000000..51ea1447 --- /dev/null +++ b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/GeneratedModuleLayout.kt @@ -0,0 +1,117 @@ +package io.github.cdsap.projectgenerator.generator + +import io.github.cdsap.projectgenerator.NameMappings +import io.github.cdsap.projectgenerator.generator.android.AndroidSourceSetLayout +import io.github.cdsap.projectgenerator.model.ModuleClassDefinitionAndroid +import io.github.cdsap.projectgenerator.model.ProjectGraph +import io.github.cdsap.projectgenerator.model.TypeProject +import java.io.File + +class GeneratedModuleLayout private constructor( + private val projectName: String, + private val layer: Int, + private val moduleId: String, + private val mainKotlinSourceDir: String, + private val testKotlinSourceDir: String, + private val resourcesSourceDir: String, + private val manifestSourceDir: String +) { + private val layerDir: String = NameMappings.layerName(layer) + private val moduleDir: String = NameMappings.moduleName(moduleId) + private val packageDir: String = NameMappings.modulePackageName(moduleId) + + fun mainKotlinPackageDir(): File = + File("$projectName/$layerDir/$moduleDir/$mainKotlinSourceDir/com/awesomeapp/$packageDir/") + + fun testKotlinPackageDir(): File = + File("$projectName/$layerDir/$moduleDir/$testKotlinSourceDir/com/awesomeapp/$packageDir/") + + fun resourcesLayoutDir(): File = + File("$projectName/$layerDir/$moduleDir/$resourcesSourceDir/layout") + + fun resourcesValuesDir(): File = + File("$projectName/$layerDir/$moduleDir/$resourcesSourceDir/values") + + fun manifestDir(): File = + File("$projectName/$layerDir/$moduleDir/$manifestSourceDir/") + + companion object { + fun of( + projectName: String, + node: ProjectGraph, + kotlinMultiplatformLibrary: Boolean + ): GeneratedModuleLayout = + of( + projectName = projectName, + layer = node.layer, + moduleId = node.id, + type = node.type, + kotlinMultiplatformLibrary = kotlinMultiplatformLibrary + ) + + fun of( + projectName: String, + moduleDefinition: ModuleClassDefinitionAndroid, + kotlinMultiplatformLibrary: Boolean + ): GeneratedModuleLayout = + of( + projectName = projectName, + layer = moduleDefinition.layer, + moduleId = moduleDefinition.moduleId, + type = moduleDefinition.projectType ?: TypeProject.ANDROID_LIB, + kotlinMultiplatformLibrary = kotlinMultiplatformLibrary + ) + + fun of( + projectName: String, + node: ProjectGraph, + mainKotlinSourceDir: String, + testKotlinSourceDir: String, + kotlinMultiplatformLibrary: Boolean = false + ): GeneratedModuleLayout = + GeneratedModuleLayout( + projectName = projectName, + layer = node.layer, + moduleId = node.id, + mainKotlinSourceDir = mainKotlinSourceDir, + testKotlinSourceDir = testKotlinSourceDir, + resourcesSourceDir = AndroidSourceSetLayout.resourcesSourceDir( + node.type, + kotlinMultiplatformLibrary + ), + manifestSourceDir = AndroidSourceSetLayout.manifestSourceDir( + node.type, + kotlinMultiplatformLibrary + ) + ) + + private fun of( + projectName: String, + layer: Int, + moduleId: String, + type: TypeProject, + kotlinMultiplatformLibrary: Boolean + ): GeneratedModuleLayout = + GeneratedModuleLayout( + projectName = projectName, + layer = layer, + moduleId = moduleId, + mainKotlinSourceDir = AndroidSourceSetLayout.kotlinMainSourceDir( + type, + kotlinMultiplatformLibrary + ), + testKotlinSourceDir = AndroidSourceSetLayout.kotlinTestSourceDir( + type, + kotlinMultiplatformLibrary + ), + resourcesSourceDir = AndroidSourceSetLayout.resourcesSourceDir( + type, + kotlinMultiplatformLibrary + ), + manifestSourceDir = AndroidSourceSetLayout.manifestSourceDir( + type, + kotlinMultiplatformLibrary + ) + ) + } +} diff --git a/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/classes/ClassGeneratorAndroid.kt b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/classes/ClassGeneratorAndroid.kt index 1c1e3f9a..2a599744 100644 --- a/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/classes/ClassGeneratorAndroid.kt +++ b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/classes/ClassGeneratorAndroid.kt @@ -1,5 +1,6 @@ package io.github.cdsap.projectgenerator.generator.classes +import io.github.cdsap.projectgenerator.generator.GeneratedModuleLayout import io.github.cdsap.projectgenerator.generator.android.AndroidSourceSetLayout import io.github.cdsap.projectgenerator.model.* import io.github.cdsap.projectgenerator.NameMappings @@ -749,15 +750,11 @@ class ClassGeneratorAndroid( moduleDefinition: ModuleClassDefinitionAndroid, projectName: String ) { - val layerDir = NameMappings.layerName(moduleDefinition.layer) - val moduleDir = NameMappings.moduleName(moduleDefinition.moduleId) - val packageDir = NameMappings.modulePackageName(moduleDefinition.moduleId) - val mainSourceDir = AndroidSourceSetLayout.kotlinMainSourceDir( - moduleDefinition.projectType ?: TypeProject.ANDROID_LIB, + val directory = GeneratedModuleLayout.of( + projectName, + moduleDefinition, kotlinMultiplatformLibrary - ) - val directory = - File("$projectName/$layerDir/$moduleDir/$mainSourceDir/com/awesomeapp/$packageDir/") + ).mainKotlinPackageDir() directory.mkdirs() val fileName = "${classDefinition.type.className()}${moduleDefinition.moduleNumber}_${classDefinition.index}.kt" diff --git a/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/resources/ResourceGenerator.kt b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/resources/ResourceGenerator.kt index 60c1e4da..d4b5c0d0 100644 --- a/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/resources/ResourceGenerator.kt +++ b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/resources/ResourceGenerator.kt @@ -1,8 +1,8 @@ package io.github.cdsap.projectgenerator.generator.resources +import io.github.cdsap.projectgenerator.generator.GeneratedModuleLayout import io.github.cdsap.projectgenerator.generator.android.ActivityLayout import io.github.cdsap.projectgenerator.generator.android.AndroidApplication -import io.github.cdsap.projectgenerator.generator.android.AndroidSourceSetLayout import io.github.cdsap.projectgenerator.generator.android.ClassTheme import io.github.cdsap.projectgenerator.generator.android.FragmentLayout import io.github.cdsap.projectgenerator.generator.android.Manifest @@ -30,12 +30,9 @@ class ResourceGenerator( typeOfStringResources: TypeOfStringResources, classesDictionary: MutableMap> ) { - // Create resource directories - val layerDir = NameMappings.layerName(node.layer) - val moduleDir = NameMappings.moduleName(node.id) - val resourcesSourceDir = AndroidSourceSetLayout.resourcesSourceDir(node.type, kotlinMultiplatformLibrary) - File("${lang.projectName}/$layerDir/$moduleDir/$resourcesSourceDir/layout").mkdirs() - File("${lang.projectName}/$layerDir/$moduleDir/$resourcesSourceDir/values").mkdirs() + val layout = GeneratedModuleLayout.of(lang.projectName, node, kotlinMultiplatformLibrary) + layout.resourcesLayoutDir().mkdirs() + layout.resourcesValuesDir().mkdirs() ClassTheme(kotlinMultiplatformLibrary).createThemeFile(node, lang) when (node.type) { @@ -75,14 +72,8 @@ class ResourceGenerator( lang: LanguageAttributes, node: ProjectGraph ): Triple { - val layerDir = NameMappings.layerName(node.layer) - val moduleDir = NameMappings.moduleName(node.id) - val resourcesSourceDir = AndroidSourceSetLayout.resourcesSourceDir(node.type, kotlinMultiplatformLibrary) - val manifestSourceDir = AndroidSourceSetLayout.manifestSourceDir(node.type, kotlinMultiplatformLibrary) - val layoutDir = File("${lang.projectName}/$layerDir/$moduleDir/$resourcesSourceDir/layout") - val valuesDir = File("${lang.projectName}/$layerDir/$moduleDir/$resourcesSourceDir/values") - val manifestDir = File("${lang.projectName}/$layerDir/$moduleDir/$manifestSourceDir/") - return Triple(layoutDir, valuesDir, manifestDir) + val layout = GeneratedModuleLayout.of(lang.projectName, node, kotlinMultiplatformLibrary) + return Triple(layout.resourcesLayoutDir(), layout.resourcesValuesDir(), layout.manifestDir()) } private fun createLayoutFiles(layoutDir: File, moduleId: String) { diff --git a/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/test/TestGeneratorAndroid.kt b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/test/TestGeneratorAndroid.kt index b7494b70..dc7056b3 100644 --- a/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/test/TestGeneratorAndroid.kt +++ b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/generator/test/TestGeneratorAndroid.kt @@ -1,6 +1,6 @@ package io.github.cdsap.projectgenerator.generator.test -import io.github.cdsap.projectgenerator.generator.android.AndroidSourceSetLayout +import io.github.cdsap.projectgenerator.generator.GeneratedModuleLayout import io.github.cdsap.projectgenerator.generator.classes.GenerateDictionaryAndroid import io.github.cdsap.projectgenerator.writer.TestGenerator import io.github.cdsap.projectgenerator.model.ClassDefinitionAndroid @@ -22,15 +22,11 @@ class TestGeneratorAndroid( projectName: String, classesDictionary: MutableMap> ) { - val layerDir = NameMappings.layerName(moduleDefinition.layer) - val moduleDir = NameMappings.moduleName(moduleDefinition.moduleId) - val packageDir = NameMappings.modulePackageName(moduleDefinition.moduleId) - val testSourceDir = AndroidSourceSetLayout.kotlinTestSourceDir( - moduleDefinition.projectType ?: io.github.cdsap.projectgenerator.model.TypeProject.ANDROID_LIB, + val testDir = GeneratedModuleLayout.of( + projectName, + moduleDefinition, kotlinMultiplatformLibrary - ) - val testDir = - File("$projectName/$layerDir/$moduleDir/$testSourceDir/com/awesomeapp/$packageDir/") + ).testKotlinPackageDir() testDir.mkdirs() moduleDefinition.classes.forEach { classDefinition -> diff --git a/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/writer/ModulesWriter.kt b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/writer/ModulesWriter.kt index e4ccfc89..7e3f26ba 100644 --- a/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/writer/ModulesWriter.kt +++ b/project-generator/src/main/kotlin/io/github/cdsap/projectgenerator/writer/ModulesWriter.kt @@ -1,14 +1,13 @@ package io.github.cdsap.projectgenerator.writer +import io.github.cdsap.projectgenerator.generator.GeneratedModuleLayout import io.github.cdsap.projectgenerator.model.LanguageAttributes -import io.github.cdsap.projectgenerator.NameMappings import io.github.cdsap.projectgenerator.model.ProjectGraph import io.github.cdsap.projectgenerator.model.TypeOfStringResources import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope -import java.io.File import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.CopyOnWriteArrayList @@ -64,15 +63,16 @@ abstract class ModulesWrite( } private fun createModuleStructure(node: ProjectGraph, lang: LanguageAttributes) { - val layerDir = NameMappings.layerName(node.layer) - val moduleDir = NameMappings.moduleName(node.id) - val packageDir = NameMappings.modulePackageName(node.id) - val mainSourceDir = sourceSetLayout.mainKotlinDir(node) - File("${lang.projectName}/$layerDir/$moduleDir/$mainSourceDir/com/awesomeapp/$packageDir/").mkdirs() + val layout = GeneratedModuleLayout.of( + projectName = lang.projectName, + node = node, + mainKotlinSourceDir = sourceSetLayout.mainKotlinDir(node), + testKotlinSourceDir = sourceSetLayout.testKotlinDir(node) + ) + layout.mainKotlinPackageDir().mkdirs() if (generateUnitTest) { - val testSourceDir = sourceSetLayout.testKotlinDir(node) - File("${lang.projectName}/$layerDir/$moduleDir/$testSourceDir/com/awesomeapp/$packageDir/").mkdirs() + layout.testKotlinPackageDir().mkdirs() } } } diff --git a/project-generator/src/test/kotlin/io/github/cdsap/projectgenerator/generator/GeneratedModuleLayoutTest.kt b/project-generator/src/test/kotlin/io/github/cdsap/projectgenerator/generator/GeneratedModuleLayoutTest.kt new file mode 100644 index 00000000..f7e46f9e --- /dev/null +++ b/project-generator/src/test/kotlin/io/github/cdsap/projectgenerator/generator/GeneratedModuleLayoutTest.kt @@ -0,0 +1,119 @@ +package io.github.cdsap.projectgenerator.generator + +import io.github.cdsap.projectgenerator.NameMappings +import io.github.cdsap.projectgenerator.model.ModuleClassDefinitionAndroid +import io.github.cdsap.projectgenerator.model.ProjectGraph +import io.github.cdsap.projectgenerator.model.TypeProject +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.io.File + +class GeneratedModuleLayoutTest { + + @Test + fun `android app uses standard main test resource and manifest paths`() { + val node = ProjectGraph("module_1_1", 1, emptyList(), TypeProject.ANDROID_APP, 1) + val layout = GeneratedModuleLayout.of("projects_generated/demo", node, kotlinMultiplatformLibrary = false) + + assertEquals(expectedPackage("projects_generated/demo", node, "src/main/kotlin"), layout.mainKotlinPackageDir()) + assertEquals(expectedPackage("projects_generated/demo", node, "src/test/kotlin"), layout.testKotlinPackageDir()) + assertEquals(expectedModuleFile("projects_generated/demo", node, "src/main/res/layout"), layout.resourcesLayoutDir()) + assertEquals(expectedModuleFile("projects_generated/demo", node, "src/main/res/values"), layout.resourcesValuesDir()) + assertEquals(expectedModuleFile("projects_generated/demo", node, "src/main/"), layout.manifestDir()) + } + + @Test + fun `android library without kmp uses standard source set paths`() { + val node = ProjectGraph("module_2_1", 2, emptyList(), TypeProject.ANDROID_LIB, 1) + val layout = GeneratedModuleLayout.of("out", node, kotlinMultiplatformLibrary = false) + + assertEquals(expectedPackage("out", node, "src/main/kotlin"), layout.mainKotlinPackageDir()) + assertEquals(expectedPackage("out", node, "src/test/kotlin"), layout.testKotlinPackageDir()) + assertEquals(expectedModuleFile("out", node, "src/main/res/layout"), layout.resourcesLayoutDir()) + assertEquals(expectedModuleFile("out", node, "src/main/res/values"), layout.resourcesValuesDir()) + assertEquals(expectedModuleFile("out", node, "src/main/"), layout.manifestDir()) + } + + @Test + fun `android kmp library uses androidMain and androidHostTest paths`() { + val node = ProjectGraph("module_3_1", 3, emptyList(), TypeProject.ANDROID_LIB, 1) + val layout = GeneratedModuleLayout.of("out", node, kotlinMultiplatformLibrary = true) + + assertEquals(expectedPackage("out", node, "src/androidMain/kotlin"), layout.mainKotlinPackageDir()) + assertEquals(expectedPackage("out", node, "src/androidHostTest/kotlin"), layout.testKotlinPackageDir()) + assertEquals(expectedModuleFile("out", node, "src/androidMain/res/layout"), layout.resourcesLayoutDir()) + assertEquals(expectedModuleFile("out", node, "src/androidMain/res/values"), layout.resourcesValuesDir()) + assertEquals(expectedModuleFile("out", node, "src/androidMain/"), layout.manifestDir()) + } + + @Test + fun `android app ignores kmp flag for source set paths`() { + val node = ProjectGraph("module_1_2", 1, emptyList(), TypeProject.ANDROID_APP, 1) + val layout = GeneratedModuleLayout.of("out", node, kotlinMultiplatformLibrary = true) + + assertEquals(expectedPackage("out", node, "src/main/kotlin"), layout.mainKotlinPackageDir()) + assertEquals(expectedPackage("out", node, "src/test/kotlin"), layout.testKotlinPackageDir()) + assertEquals(expectedModuleFile("out", node, "src/main/res/layout"), layout.resourcesLayoutDir()) + assertEquals(expectedModuleFile("out", node, "src/main/"), layout.manifestDir()) + } + + @Test + fun `module definition factory uses project type with android lib default`() { + val withType = ModuleClassDefinitionAndroid( + moduleId = "module_4_1", + layer = 4, + moduleNumber = 4, + classes = emptyList(), + projectType = TypeProject.ANDROID_APP + ) + val withoutType = ModuleClassDefinitionAndroid( + moduleId = "module_5_1", + layer = 5, + moduleNumber = 5, + classes = emptyList() + ) + + val appLayout = GeneratedModuleLayout.of("out", withType, kotlinMultiplatformLibrary = true) + val libLayout = GeneratedModuleLayout.of("out", withoutType, kotlinMultiplatformLibrary = true) + + assertEquals( + File("out/${NameMappings.layerName(4)}/module_4_1/src/main/kotlin/com/awesomeapp/module_4_1/"), + appLayout.mainKotlinPackageDir() + ) + assertEquals( + File("out/${NameMappings.layerName(5)}/module_5_1/src/androidMain/kotlin/com/awesomeapp/module_5_1/"), + libLayout.mainKotlinPackageDir() + ) + assertEquals( + File("out/${NameMappings.layerName(5)}/module_5_1/src/androidHostTest/kotlin/com/awesomeapp/module_5_1/"), + libLayout.testKotlinPackageDir() + ) + } + + @Test + fun `writer overload preserves injected kotlin source directories`() { + val node = ProjectGraph("module_1_1", 1, emptyList(), TypeProject.LIB, 1) + val layout = GeneratedModuleLayout.of( + projectName = "out", + node = node, + mainKotlinSourceDir = "src/customMain/kotlin", + testKotlinSourceDir = "src/customTest/kotlin" + ) + + assertEquals(expectedPackage("out", node, "src/customMain/kotlin"), layout.mainKotlinPackageDir()) + assertEquals(expectedPackage("out", node, "src/customTest/kotlin"), layout.testKotlinPackageDir()) + } + + private fun expectedPackage(projectName: String, node: ProjectGraph, sourceDir: String): File { + val layerDir = NameMappings.layerName(node.layer) + val moduleDir = NameMappings.moduleName(node.id) + val packageDir = NameMappings.modulePackageName(node.id) + return File("$projectName/$layerDir/$moduleDir/$sourceDir/com/awesomeapp/$packageDir/") + } + + private fun expectedModuleFile(projectName: String, node: ProjectGraph, relativePath: String): File { + val layerDir = NameMappings.layerName(node.layer) + val moduleDir = NameMappings.moduleName(node.id) + return File("$projectName/$layerDir/$moduleDir/$relativePath") + } +}