Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions android/bin/main/com/rees46/android/AndroidPlugin.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.rees46.android

import com.android.build.gradle.BaseExtension
import org.gradle.api.JavaVersion
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension

class AndroidPlugin : Plugin<Project> {
override fun apply(target: Project) {
target.pluginManager.apply("org.jetbrains.kotlin.multiplatform")
target.pluginManager.apply("com.android.library")

target.extensions.configure<KotlinMultiplatformExtension> {
androidTarget {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_22)
}
}
}

target.extensions.configure<BaseExtension> {
compileSdkVersion(35)
defaultConfig {
minSdk = 24
targetSdk = 34
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_22
targetCompatibility = JavaVersion.VERSION_22
}
}
}
}
7 changes: 6 additions & 1 deletion android/src/main/kotlin/com/rees46/android/AndroidPlugin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import org.gradle.api.JavaVersion
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension

class AndroidPlugin : Plugin<Project> {
Expand All @@ -13,7 +14,11 @@ class AndroidPlugin : Plugin<Project> {
target.pluginManager.apply("com.android.library")

target.extensions.configure<KotlinMultiplatformExtension> {
androidTarget()
androidTarget {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_22)
}
}
}

target.extensions.configure<BaseExtension> {
Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ plugins {
}

dependencies {
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.0")
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.21")
implementation("com.android.tools.build:gradle:8.9.0")
}

Expand Down
41 changes: 41 additions & 0 deletions compose/bin/main/com/rees46/compose/ComposePlugin.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.rees46.compose

import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.getByType
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension

const val composeVersion = "1.7.0"
const val androidxLifecycleVersion = "2.8.4"

class ComposePlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
pluginManager.apply("org.jetbrains.kotlin.multiplatform")
pluginManager.apply("org.jetbrains.compose")
pluginManager.apply("org.jetbrains.kotlin.plugin.compose")
pluginManager.apply("com.rees46.ios")
pluginManager.apply("com.rees46.android")

val composeDependencies = listOf(
"org.jetbrains.compose.runtime:runtime:$composeVersion",
"org.jetbrains.compose.foundation:foundation:$composeVersion",
"org.jetbrains.compose.material:material:$composeVersion",
"org.jetbrains.compose.ui:ui:$composeVersion",
"org.jetbrains.compose.components:components-resources:$composeVersion",
"org.jetbrains.compose.components:components-ui-tooling-preview:$composeVersion"
)

extensions.getByType<KotlinMultiplatformExtension>().apply {
sourceSets.getByName("commonMain").dependencies {
composeDependencies.forEach { implementation(it) }
}
sourceSets.getByName("androidMain").dependencies {
implementation("androidx.lifecycle:lifecycle-viewmodel:$androidxLifecycleVersion")
implementation("androidx.lifecycle:lifecycle-runtime-compose:$androidxLifecycleVersion")
implementation("org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:${androidxLifecycleVersion}")
}
}
}
}
}
16 changes: 16 additions & 0 deletions generator/bin/main/com/rees46/generator/ModuleGeneratorPlugin.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.rees46.generator

import com.rees46.generator.tasks.compose.GenerateComposeModuleTask
import com.rees46.generator.tasks.kmp.GenerateKmpModuleTask
import org.gradle.api.Plugin
import org.gradle.api.Project

const val kmpTask = "generateKmpModule"
const val composeTask = "generateComposeModule"

class ModuleGeneratorPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.tasks.register(kmpTask, GenerateKmpModuleTask::class.java)
project.tasks.register(composeTask, GenerateComposeModuleTask::class.java)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.rees46.generator.compose

import com.rees46.generator.core.ModuleGenerator
import org.gradle.api.Project

class ComposeModuleGenerator(
project: Project,
moduleName: String,
organization: String
) : ModuleGenerator(project, moduleName, organization) {
override val gradleTemplatePath = "templates/compose/compose.build.gradle.kts.template"
override val kotlinTemplatePath = "templates/compose/ComposeBaseFile.kt.template"
}
152 changes: 152 additions & 0 deletions generator/bin/main/com/rees46/generator/core/ModuleGenerator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package com.rees46.generator.core

import org.gradle.api.Project
import org.gradle.api.logging.Logger
import java.io.File
import java.io.IOException

abstract class ModuleGenerator(
protected val project: Project,
protected val modulePath: String,
protected val organization: String
) {
protected abstract val gradleTemplatePath: String
protected abstract val kotlinTemplatePath: String

protected val logger: Logger = project.logger
private val pathComponents: List<String>
private val moduleName: String
private val parentDirs: List<String>

init {
require(modulePath.isNotBlank()) { "Module path cannot be blank" }
require(organization.isNotBlank()) { "Organization cannot be blank" }

pathComponents = modulePath.split(":").filter { it.isNotBlank() }
require(pathComponents.isNotEmpty()) { "Module path cannot be empty" }
require(pathComponents.all { it.isNotBlank() }) { "Module path components cannot be blank" }

moduleName = pathComponents.last()
parentDirs = pathComponents.dropLast(1)
}

fun generate() {
try {
val moduleDir = resolveModuleDir()
validateModuleDoesNotExist(moduleDir)

createDirectoryStructure(moduleDir)
generateGradleFile(moduleDir)
generateKotlinFiles(moduleDir)
addGitignore(moduleDir)
addToSettingsGradle()

logger.lifecycle("Created module '$modulePath' at ${moduleDir.relativeTo(project.rootDir)}")
} catch (e: Exception) {
logger.error("Failed to generate module '$modulePath'", e)
throw e
}
}

private fun resolveModuleDir(): File {
return parentDirs.fold(project.rootDir) { dir, name ->
File(dir, name).also {
if (!it.exists() && !it.mkdirs()) {
throw IOException("Failed to create directory: ${it.absolutePath}")
}
}
}.resolve(moduleName)
}

private fun validateModuleDoesNotExist(moduleDir: File) {
if (moduleDir.exists()) {
throw IllegalStateException(
"Module '$modulePath' already exists at ${moduleDir.relativeTo(project.rootDir)}"
)
}
}

protected open fun createDirectoryStructure(moduleDir: File) {
val kotlinPath = "src/commonMain/kotlin/${organization.replace(".", "/")}/$moduleName"
File(moduleDir, kotlinPath).mkdirs().also { success ->
if (!success) {
throw IOException("Failed to create directory structure in ${moduleDir.absolutePath}")
}
}
}

protected open fun generateGradleFile(moduleDir: File) {
try {
val template = readResource(gradleTemplatePath)
.replace("{{PACKAGE}}", "$organization.$moduleName")
.replace("{{BASENAME}}", "${moduleName}Kit")

File(moduleDir, "build.gradle.kts").writeText(template)
} catch (e: Exception) {
throw IllegalStateException("Failed to generate Gradle file", e)
}
}

protected open fun generateKotlinFiles(moduleDir: File) {
try {
val template = readResource(kotlinTemplatePath)
.replace("{{PACKAGE}}", "$organization.$moduleName")
.replace("{{MODULENAME}}", moduleName)

val kotlinPath = "src/commonMain/kotlin/${organization.replace(".", "/")}/$moduleName"
File(moduleDir, "$kotlinPath/Greeting.kt").writeText(template)
} catch (e: Exception) {
throw IllegalStateException("Failed to generate Kotlin files", e)
}
}

private fun addGitignore(moduleDir: File) {
try {
val gitignoreContent = """
/build
""".trimIndent()

File(moduleDir, ".gitignore").writeText(gitignoreContent)
logger.debug("Added .gitignore to ${moduleDir.relativeTo(project.rootDir)}")
} catch (e: Exception) {
logger.warn("Failed to add .gitignore file", e)
}
}

private fun addToSettingsGradle() {
val settingsFile = project.rootDir.resolve("settings.gradle.kts")
if (!settingsFile.exists()) {
logger.warn("settings.gradle.kts not found in project root")
return
}

val includePath = ":" + modulePath.replace(":", ":")
val includeStatement = "include(\"$includePath\")"

try {
val content = settingsFile.readText()
val pattern = Regex("""include\s*\(["']$includePath["']\)""")
if (pattern.containsMatchIn(content)) {
logger.lifecycle("Module '$includePath' is already included in settings.gradle.kts")
return
}

val newContent = if (content.isNotBlank() && !content.endsWith("\n")) {
"$content\n$includeStatement\n"
} else {
"$content$includeStatement\n"
}

settingsFile.writeText(newContent)
logger.lifecycle("Added to settings.gradle.kts: $includeStatement")
} catch (e: Exception) {
throw IllegalStateException("Failed to update settings.gradle.kts", e)
}
}

protected fun readResource(path: String): String {
return javaClass.classLoader.getResourceAsStream(path)?.use { stream ->
stream.bufferedReader().use { it.readText() }
} ?: throw IllegalStateException("Resource '$path' not found. Make sure the template exists.")
}
}
13 changes: 13 additions & 0 deletions generator/bin/main/com/rees46/generator/kmp/KmpModuleGenerator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.rees46.generator.kmp

import com.rees46.generator.core.ModuleGenerator
import org.gradle.api.Project

class KmpModuleGenerator(
project: Project,
moduleName: String,
organization: String
) : ModuleGenerator(project, moduleName, organization) {
override val gradleTemplatePath = "templates/kmp/kmp.build.gradle.kts.template"
override val kotlinTemplatePath = "templates/kmp/KmpBaseFile.kt.template"
}
17 changes: 17 additions & 0 deletions generator/bin/main/com/rees46/generator/tasks/BaseGeneratorTask.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.rees46.generator.tasks

import org.gradle.api.DefaultTask
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.options.Option

abstract class BaseGeneratorTask : DefaultTask() {

@get:Input
@get:Option(option = "moduleName", description = "Name of the module to generate")
abstract val moduleName: Property<String>

@get:Input
@get:Option(option = "organization", description = "Organization (package prefix)")
abstract val organization: Property<String>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.rees46.generator.tasks.compose

import com.rees46.generator.compose.ComposeModuleGenerator
import com.rees46.generator.tasks.BaseGeneratorTask
import org.gradle.api.tasks.TaskAction

abstract class GenerateComposeModuleTask : BaseGeneratorTask() {

init {
group = "generator"
description = "Generates a new Compose module"
}

@TaskAction
fun generate() {
ComposeModuleGenerator(
project = project,
moduleName = moduleName.get(),
organization = organization.get()
).generate()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.rees46.generator.tasks.kmp

import com.rees46.generator.kmp.KmpModuleGenerator
import com.rees46.generator.tasks.BaseGeneratorTask
import org.gradle.api.tasks.TaskAction

abstract class GenerateKmpModuleTask : BaseGeneratorTask() {

init {
group = "generator"
description = "Generates a new KMP module"
}

@TaskAction
fun generate() {
KmpModuleGenerator(
project = project,
moduleName = moduleName.get(),
organization = organization.get()
).generate()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package {{PACKAGE}}

import androidx.compose.material.Text
import androidx.compose.runtime.Composable

@Composable
fun Greeting() {
Text(text = "Hello compose module!")
}
Loading