From 68655eae1a23edf5cb1cf446e677830cb24d54a6 Mon Sep 17 00:00:00 2001 From: ulfrxdev Date: Fri, 24 Apr 2026 20:21:03 +0200 Subject: [PATCH] Phase 1 work --- .editorconfig | 21 ++++++++++++++++ .planning/STATE.md | 14 +++++------ .../recipe.kotlin.multiplatform.gradle.kts | 14 +++++++++++ composeApp/build.gradle.kts | 5 +++- .../kotlin/dev/ulfrx/recipe/MainActivity.kt | 2 +- .../commonMain/kotlin/dev/ulfrx/recipe/App.kt | 18 ++++++++------ .../kotlin/dev/ulfrx/recipe/di/AppModule.kt | 7 +++--- .../kotlin/dev/ulfrx/recipe/di/Koin.kt | 9 +++---- .../dev/ulfrx/recipe/ComposeAppCommonTest.kt | 3 +-- .../dev/ulfrx/recipe/MainViewController.kt | 2 +- .../kotlin/dev/ulfrx/recipe/Application.kt | 4 +++- .../main/kotlin/dev/ulfrx/recipe/Database.kt | 18 ++++++++++---- .../dev/ulfrx/recipe/ApplicationTest.kt | 24 +++++++++---------- shared/build.gradle.kts | 15 +++++++++--- .../dev/ulfrx/recipe/Platform.android.kt | 4 ++-- .../kotlin/dev/ulfrx/recipe/Constants.kt | 2 +- .../kotlin/dev/ulfrx/recipe/Greeting.kt | 8 +++---- .../kotlin/dev/ulfrx/recipe/Platform.kt | 6 ++--- .../dev/ulfrx/recipe/SharedCommonTest.kt | 3 +-- .../kotlin/dev/ulfrx/recipe/Platform.ios.kt | 4 ++-- .../kotlin/dev/ulfrx/recipe/Platform.jvm.kt | 4 ++-- .../dev/ulfrx/recipe/Platform.wasmJs.kt | 4 ++-- 22 files changed, 126 insertions(+), 65 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f27757e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +[*.{kt,kts}] +# ktlint configuration for Compose Multiplatform. +# - function-naming is disabled because @Composable functions and Kotlin/Native +# entry-point factories (e.g. MainViewController) are PascalCase by convention. +# - filename is disabled because Compose-Multiplatform entry-point files +# (jvmMain/main.kt, webMain/main.kt) follow the Kotlin `fun main()` convention. +ktlint_standard_function-naming = disabled +ktlint_standard_filename = disabled + +[*.md] +trim_trailing_whitespace = false diff --git a/.planning/STATE.md b/.planning/STATE.md index 36b2827..84db9bc 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -4,13 +4,13 @@ milestone: v1.0 milestone_name: milestone current_plan: 1 status: executing -last_updated: "2026-04-24T16:11:23.051Z" +last_updated: "2026-04-24T17:39:22.205Z" progress: total_phases: 11 completed_phases: 0 total_plans: 7 - completed_plans: 0 - percent: 0 + completed_plans: 4 + percent: 57 --- # Project State: Recipe @@ -25,11 +25,11 @@ progress: ## Current Position -Phase: 01 (Project Infrastructure & Module Wiring) — EXECUTING -Plan: 1 of 7 -**Current focus:** Phase 01 — Project Infrastructure & Module Wiring +Phase: --phase (01) — EXECUTING +Plan: 1 of --name +**Current focus:** Phase --phase — 01 **Current plan:** 1 -**Status:** Executing Phase 01 +**Status:** Executing Phase --phase **Phase progress:** 0 / 11 phases complete **Progress bar:** `░░░░░░░░░░░░░░░░░░░░` 0% diff --git a/build-logic/src/main/kotlin/recipe.kotlin.multiplatform.gradle.kts b/build-logic/src/main/kotlin/recipe.kotlin.multiplatform.gradle.kts index 9cc76d2..bab2c7f 100644 --- a/build-logic/src/main/kotlin/recipe.kotlin.multiplatform.gradle.kts +++ b/build-logic/src/main/kotlin/recipe.kotlin.multiplatform.gradle.kts @@ -53,3 +53,17 @@ kotlin { } } } + +// Relax allWarningsAsErrors for KLIB-merging metadata tasks. KotlinCompileCommon +// aggregates dependency KLIBs and surfaces upstream "duplicated unique_name" +// resolver warnings caused by androidx.lifecycle 2.10.0 (Android-only) and +// org.jetbrains.androidx.lifecycle 2.10.0 (CMP) co-publishing artifacts with +// matching KLIB unique_names. This is an upstream Compose-Multiplatform 1.10 + +// lifecycle 2.10 ecosystem condition (KT-62515-style), not actionable in our +// source — so we keep -Werror on real source compilation tasks but disable it +// for the metadata-aggregation step where no user code is being compiled. +tasks.withType().configureEach { + compilerOptions { + allWarningsAsErrors.set(false) + } +} diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index bbc7687..6f333b4 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -1,7 +1,10 @@ plugins { + // AGP must apply BEFORE recipe.kotlin.multiplatform — the latter calls androidTarget(), + // which requires the Android Gradle Plugin to already be on the project. Gradle applies + // plugin IDs in declaration order, so recipe.android.application is listed first. + id("recipe.android.application") id("recipe.kotlin.multiplatform") id("recipe.compose.multiplatform") - id("recipe.android.application") id("recipe.quality") } diff --git a/composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainActivity.kt b/composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainActivity.kt index 53747b6..3174137 100644 --- a/composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainActivity.kt +++ b/composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainActivity.kt @@ -22,4 +22,4 @@ class MainActivity : ComponentActivity() { @Composable fun AppAndroidPreview() { App() -} \ No newline at end of file +} diff --git a/composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt b/composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt index b583f24..6243243 100644 --- a/composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt +++ b/composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt @@ -10,12 +10,15 @@ import androidx.compose.foundation.layout.safeContentPadding import androidx.compose.material3.Button import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import org.jetbrains.compose.resources.painterResource - import recipe.composeapp.generated.resources.Res import recipe.composeapp.generated.resources.compose_multiplatform @@ -25,10 +28,11 @@ fun App() { MaterialTheme { var showContent by remember { mutableStateOf(false) } Column( - modifier = Modifier - .background(MaterialTheme.colorScheme.primaryContainer) - .safeContentPadding() - .fillMaxSize(), + modifier = + Modifier + .background(MaterialTheme.colorScheme.primaryContainer) + .safeContentPadding() + .fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, ) { Button(onClick = { showContent = !showContent }) { @@ -46,4 +50,4 @@ fun App() { } } } -} \ No newline at end of file +} diff --git a/composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt b/composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt index f0461a6..337f956 100644 --- a/composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt +++ b/composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt @@ -3,6 +3,7 @@ package dev.ulfrx.recipe.di import org.koin.dsl.module // Phase 2 adds authModule; Phase 4 adds syncModule; Phase 5 adds catalogModule; etc. -val appModule = module { - // intentionally empty in Phase 1 -} +val appModule = + module { + // intentionally empty in Phase 1 + } diff --git a/composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt b/composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt index 1ce7e87..42158f3 100644 --- a/composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt +++ b/composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/Koin.kt @@ -4,7 +4,8 @@ import org.koin.core.KoinApplication import org.koin.core.context.startKoin import org.koin.dsl.KoinAppDeclaration -fun initKoin(config: KoinAppDeclaration? = null): KoinApplication = startKoin { - config?.invoke(this) - modules(appModule) -} +fun initKoin(config: KoinAppDeclaration? = null): KoinApplication = + startKoin { + config?.invoke(this) + modules(appModule) + } diff --git a/composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ComposeAppCommonTest.kt b/composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ComposeAppCommonTest.kt index 5823b1c..28d5fd3 100644 --- a/composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ComposeAppCommonTest.kt +++ b/composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ComposeAppCommonTest.kt @@ -4,9 +4,8 @@ import kotlin.test.Test import kotlin.test.assertEquals class ComposeAppCommonTest { - @Test fun example() { assertEquals(3, 1 + 2) } -} \ No newline at end of file +} diff --git a/composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/MainViewController.kt b/composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/MainViewController.kt index 9bf1a9a..5a176b2 100644 --- a/composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/MainViewController.kt +++ b/composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/MainViewController.kt @@ -2,4 +2,4 @@ package dev.ulfrx.recipe import androidx.compose.ui.window.ComposeUIViewController -fun MainViewController() = ComposeUIViewController { App() } \ No newline at end of file +fun MainViewController() = ComposeUIViewController { App() } diff --git a/server/src/main/kotlin/dev/ulfrx/recipe/Application.kt b/server/src/main/kotlin/dev/ulfrx/recipe/Application.kt index 8687ae3..a733627 100644 --- a/server/src/main/kotlin/dev/ulfrx/recipe/Application.kt +++ b/server/src/main/kotlin/dev/ulfrx/recipe/Application.kt @@ -17,7 +17,9 @@ fun main() { } @Serializable -private data class Health(val status: String) +private data class Health( + val status: String, +) fun Application.module() { install(ContentNegotiation) { diff --git a/server/src/main/kotlin/dev/ulfrx/recipe/Database.kt b/server/src/main/kotlin/dev/ulfrx/recipe/Database.kt index f7cde44..5876dd6 100644 --- a/server/src/main/kotlin/dev/ulfrx/recipe/Database.kt +++ b/server/src/main/kotlin/dev/ulfrx/recipe/Database.kt @@ -8,14 +8,24 @@ object Database { private val log = LoggerFactory.getLogger(Database::class.java) fun migrate(app: Application) { - val url = app.environment.config.property("database.url").getString() - val user = app.environment.config.property("database.user").getString() - val password = app.environment.config.property("database.password").getString() + val url = + app.environment.config + .property("database.url") + .getString() + val user = + app.environment.config + .property("database.user") + .getString() + val password = + app.environment.config + .property("database.password") + .getString() log.info("Connecting to {} as {} and running Flyway migrations", url, user) runCatching { - Flyway.configure() + Flyway + .configure() .dataSource(url, user, password) .locations("classpath:db/migration") .baselineOnMigrate(true) diff --git a/server/src/test/kotlin/dev/ulfrx/recipe/ApplicationTest.kt b/server/src/test/kotlin/dev/ulfrx/recipe/ApplicationTest.kt index 6f9f71a..edee832 100644 --- a/server/src/test/kotlin/dev/ulfrx/recipe/ApplicationTest.kt +++ b/server/src/test/kotlin/dev/ulfrx/recipe/ApplicationTest.kt @@ -12,19 +12,19 @@ import kotlin.test.assertEquals import kotlin.test.assertTrue class ApplicationTest { - @Test - fun `health endpoint returns 200 with status ok`() = testApplication { - application { - install(ContentNegotiation) { - json() + fun `health endpoint returns 200 with status ok`() = + testApplication { + application { + install(ContentNegotiation) { + json() + } + configureRouting() } - configureRouting() + val response = client.get("/health") + assertEquals(HttpStatusCode.OK, response.status) + val body = response.bodyAsText() + assertTrue(body.contains("\"status\""), "expected body to contain status field, was: $body") + assertTrue(body.contains("\"ok\""), "expected body to contain ok value, was: $body") } - val response = client.get("/health") - assertEquals(HttpStatusCode.OK, response.status) - val body = response.bodyAsText() - assertTrue(body.contains("\"status\""), "expected body to contain status field, was: $body") - assertTrue(body.contains("\"ok\""), "expected body to contain ok value, was: $body") - } } diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index d4bf9e5..88161b8 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -1,7 +1,10 @@ plugins { + // AGP must apply BEFORE recipe.kotlin.multiplatform — the latter calls androidTarget(), + // which requires the Android Gradle Plugin to already be on the project. Gradle applies + // plugin IDs in declaration order, so com.android.library is listed first. + alias(libs.plugins.androidLibrary) id("recipe.kotlin.multiplatform") id("recipe.quality") - alias(libs.plugins.androidLibrary) } kotlin { @@ -25,12 +28,18 @@ kotlin { android { namespace = "dev.ulfrx.recipe.shared" - compileSdk = libs.versions.android.compileSdk.get().toInt() + compileSdk = + libs.versions.android.compileSdk + .get() + .toInt() compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } defaultConfig { - minSdk = libs.versions.android.minSdk.get().toInt() + minSdk = + libs.versions.android.minSdk + .get() + .toInt() } } diff --git a/shared/src/androidMain/kotlin/dev/ulfrx/recipe/Platform.android.kt b/shared/src/androidMain/kotlin/dev/ulfrx/recipe/Platform.android.kt index 0ae5fce..113f24d 100644 --- a/shared/src/androidMain/kotlin/dev/ulfrx/recipe/Platform.android.kt +++ b/shared/src/androidMain/kotlin/dev/ulfrx/recipe/Platform.android.kt @@ -2,8 +2,8 @@ package dev.ulfrx.recipe import android.os.Build -class AndroidPlatform : Platform { +public class AndroidPlatform : Platform { override val name: String = "Android ${Build.VERSION.SDK_INT}" } -actual fun getPlatform(): Platform = AndroidPlatform() \ No newline at end of file +public actual fun getPlatform(): Platform = AndroidPlatform() diff --git a/shared/src/commonMain/kotlin/dev/ulfrx/recipe/Constants.kt b/shared/src/commonMain/kotlin/dev/ulfrx/recipe/Constants.kt index 685cd02..e92732a 100644 --- a/shared/src/commonMain/kotlin/dev/ulfrx/recipe/Constants.kt +++ b/shared/src/commonMain/kotlin/dev/ulfrx/recipe/Constants.kt @@ -1,3 +1,3 @@ package dev.ulfrx.recipe -const val SERVER_PORT = 8080 \ No newline at end of file +public const val SERVER_PORT: Int = 8080 diff --git a/shared/src/commonMain/kotlin/dev/ulfrx/recipe/Greeting.kt b/shared/src/commonMain/kotlin/dev/ulfrx/recipe/Greeting.kt index 71380cf..f1642d7 100644 --- a/shared/src/commonMain/kotlin/dev/ulfrx/recipe/Greeting.kt +++ b/shared/src/commonMain/kotlin/dev/ulfrx/recipe/Greeting.kt @@ -1,9 +1,7 @@ package dev.ulfrx.recipe -class Greeting { +public class Greeting { private val platform = getPlatform() - fun greet(): String { - return "Hello, ${platform.name}!" - } -} \ No newline at end of file + public fun greet(): String = "Hello, ${platform.name}!" +} diff --git a/shared/src/commonMain/kotlin/dev/ulfrx/recipe/Platform.kt b/shared/src/commonMain/kotlin/dev/ulfrx/recipe/Platform.kt index 0b19808..e56f649 100644 --- a/shared/src/commonMain/kotlin/dev/ulfrx/recipe/Platform.kt +++ b/shared/src/commonMain/kotlin/dev/ulfrx/recipe/Platform.kt @@ -1,7 +1,7 @@ package dev.ulfrx.recipe -interface Platform { - val name: String +public interface Platform { + public val name: String } -expect fun getPlatform(): Platform \ No newline at end of file +public expect fun getPlatform(): Platform diff --git a/shared/src/commonTest/kotlin/dev/ulfrx/recipe/SharedCommonTest.kt b/shared/src/commonTest/kotlin/dev/ulfrx/recipe/SharedCommonTest.kt index e8a9b49..2e3239b 100644 --- a/shared/src/commonTest/kotlin/dev/ulfrx/recipe/SharedCommonTest.kt +++ b/shared/src/commonTest/kotlin/dev/ulfrx/recipe/SharedCommonTest.kt @@ -4,9 +4,8 @@ import kotlin.test.Test import kotlin.test.assertEquals class SharedCommonTest { - @Test fun example() { assertEquals(3, 1 + 2) } -} \ No newline at end of file +} diff --git a/shared/src/iosMain/kotlin/dev/ulfrx/recipe/Platform.ios.kt b/shared/src/iosMain/kotlin/dev/ulfrx/recipe/Platform.ios.kt index 089043d..bde5495 100644 --- a/shared/src/iosMain/kotlin/dev/ulfrx/recipe/Platform.ios.kt +++ b/shared/src/iosMain/kotlin/dev/ulfrx/recipe/Platform.ios.kt @@ -2,8 +2,8 @@ package dev.ulfrx.recipe import platform.UIKit.UIDevice -class IOSPlatform : Platform { +public class IOSPlatform : Platform { override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion } -actual fun getPlatform(): Platform = IOSPlatform() \ No newline at end of file +public actual fun getPlatform(): Platform = IOSPlatform() diff --git a/shared/src/jvmMain/kotlin/dev/ulfrx/recipe/Platform.jvm.kt b/shared/src/jvmMain/kotlin/dev/ulfrx/recipe/Platform.jvm.kt index ed01655..c7125e7 100644 --- a/shared/src/jvmMain/kotlin/dev/ulfrx/recipe/Platform.jvm.kt +++ b/shared/src/jvmMain/kotlin/dev/ulfrx/recipe/Platform.jvm.kt @@ -1,7 +1,7 @@ package dev.ulfrx.recipe -class JVMPlatform : Platform { +public class JVMPlatform : Platform { override val name: String = "Java ${System.getProperty("java.version")}" } -actual fun getPlatform(): Platform = JVMPlatform() \ No newline at end of file +public actual fun getPlatform(): Platform = JVMPlatform() diff --git a/shared/src/wasmJsMain/kotlin/dev/ulfrx/recipe/Platform.wasmJs.kt b/shared/src/wasmJsMain/kotlin/dev/ulfrx/recipe/Platform.wasmJs.kt index f4fc33c..d166afe 100644 --- a/shared/src/wasmJsMain/kotlin/dev/ulfrx/recipe/Platform.wasmJs.kt +++ b/shared/src/wasmJsMain/kotlin/dev/ulfrx/recipe/Platform.wasmJs.kt @@ -1,7 +1,7 @@ package dev.ulfrx.recipe -class WasmPlatform : Platform { +public class WasmPlatform : Platform { override val name: String = "Web with Kotlin/Wasm" } -actual fun getPlatform(): Platform = WasmPlatform() \ No newline at end of file +public actual fun getPlatform(): Platform = WasmPlatform()