Phase 1 work
This commit is contained in:
21
.editorconfig
Normal file
21
.editorconfig
Normal file
@@ -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
|
||||||
@@ -4,13 +4,13 @@ milestone: v1.0
|
|||||||
milestone_name: milestone
|
milestone_name: milestone
|
||||||
current_plan: 1
|
current_plan: 1
|
||||||
status: executing
|
status: executing
|
||||||
last_updated: "2026-04-24T16:11:23.051Z"
|
last_updated: "2026-04-24T17:39:22.205Z"
|
||||||
progress:
|
progress:
|
||||||
total_phases: 11
|
total_phases: 11
|
||||||
completed_phases: 0
|
completed_phases: 0
|
||||||
total_plans: 7
|
total_plans: 7
|
||||||
completed_plans: 0
|
completed_plans: 4
|
||||||
percent: 0
|
percent: 57
|
||||||
---
|
---
|
||||||
|
|
||||||
# Project State: Recipe
|
# Project State: Recipe
|
||||||
@@ -25,11 +25,11 @@ progress:
|
|||||||
|
|
||||||
## Current Position
|
## Current Position
|
||||||
|
|
||||||
Phase: 01 (Project Infrastructure & Module Wiring) — EXECUTING
|
Phase: --phase (01) — EXECUTING
|
||||||
Plan: 1 of 7
|
Plan: 1 of --name
|
||||||
**Current focus:** Phase 01 — Project Infrastructure & Module Wiring
|
**Current focus:** Phase --phase — 01
|
||||||
**Current plan:** 1
|
**Current plan:** 1
|
||||||
**Status:** Executing Phase 01
|
**Status:** Executing Phase --phase
|
||||||
**Phase progress:** 0 / 11 phases complete
|
**Phase progress:** 0 / 11 phases complete
|
||||||
**Progress bar:** `░░░░░░░░░░░░░░░░░░░░` 0%
|
**Progress bar:** `░░░░░░░░░░░░░░░░░░░░` 0%
|
||||||
|
|
||||||
|
|||||||
@@ -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<org.jetbrains.kotlin.gradle.tasks.KotlinCompileCommon>().configureEach {
|
||||||
|
compilerOptions {
|
||||||
|
allWarningsAsErrors.set(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
plugins {
|
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.kotlin.multiplatform")
|
||||||
id("recipe.compose.multiplatform")
|
id("recipe.compose.multiplatform")
|
||||||
id("recipe.android.application")
|
|
||||||
id("recipe.quality")
|
id("recipe.quality")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,4 +22,4 @@ class MainActivity : ComponentActivity() {
|
|||||||
@Composable
|
@Composable
|
||||||
fun AppAndroidPreview() {
|
fun AppAndroidPreview() {
|
||||||
App()
|
App()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,15 @@ import androidx.compose.foundation.layout.safeContentPadding
|
|||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
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.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import org.jetbrains.compose.resources.painterResource
|
import org.jetbrains.compose.resources.painterResource
|
||||||
|
|
||||||
import recipe.composeapp.generated.resources.Res
|
import recipe.composeapp.generated.resources.Res
|
||||||
import recipe.composeapp.generated.resources.compose_multiplatform
|
import recipe.composeapp.generated.resources.compose_multiplatform
|
||||||
|
|
||||||
@@ -25,10 +28,11 @@ fun App() {
|
|||||||
MaterialTheme {
|
MaterialTheme {
|
||||||
var showContent by remember { mutableStateOf(false) }
|
var showContent by remember { mutableStateOf(false) }
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.background(MaterialTheme.colorScheme.primaryContainer)
|
Modifier
|
||||||
.safeContentPadding()
|
.background(MaterialTheme.colorScheme.primaryContainer)
|
||||||
.fillMaxSize(),
|
.safeContentPadding()
|
||||||
|
.fillMaxSize(),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
Button(onClick = { showContent = !showContent }) {
|
Button(onClick = { showContent = !showContent }) {
|
||||||
@@ -46,4 +50,4 @@ fun App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package dev.ulfrx.recipe.di
|
|||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
// Phase 2 adds authModule; Phase 4 adds syncModule; Phase 5 adds catalogModule; etc.
|
// Phase 2 adds authModule; Phase 4 adds syncModule; Phase 5 adds catalogModule; etc.
|
||||||
val appModule = module {
|
val appModule =
|
||||||
// intentionally empty in Phase 1
|
module {
|
||||||
}
|
// intentionally empty in Phase 1
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import org.koin.core.KoinApplication
|
|||||||
import org.koin.core.context.startKoin
|
import org.koin.core.context.startKoin
|
||||||
import org.koin.dsl.KoinAppDeclaration
|
import org.koin.dsl.KoinAppDeclaration
|
||||||
|
|
||||||
fun initKoin(config: KoinAppDeclaration? = null): KoinApplication = startKoin {
|
fun initKoin(config: KoinAppDeclaration? = null): KoinApplication =
|
||||||
config?.invoke(this)
|
startKoin {
|
||||||
modules(appModule)
|
config?.invoke(this)
|
||||||
}
|
modules(appModule)
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,9 +4,8 @@ import kotlin.test.Test
|
|||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
class ComposeAppCommonTest {
|
class ComposeAppCommonTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun example() {
|
fun example() {
|
||||||
assertEquals(3, 1 + 2)
|
assertEquals(3, 1 + 2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ package dev.ulfrx.recipe
|
|||||||
|
|
||||||
import androidx.compose.ui.window.ComposeUIViewController
|
import androidx.compose.ui.window.ComposeUIViewController
|
||||||
|
|
||||||
fun MainViewController() = ComposeUIViewController { App() }
|
fun MainViewController() = ComposeUIViewController { App() }
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ fun main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
private data class Health(val status: String)
|
private data class Health(
|
||||||
|
val status: String,
|
||||||
|
)
|
||||||
|
|
||||||
fun Application.module() {
|
fun Application.module() {
|
||||||
install(ContentNegotiation) {
|
install(ContentNegotiation) {
|
||||||
|
|||||||
@@ -8,14 +8,24 @@ object Database {
|
|||||||
private val log = LoggerFactory.getLogger(Database::class.java)
|
private val log = LoggerFactory.getLogger(Database::class.java)
|
||||||
|
|
||||||
fun migrate(app: Application) {
|
fun migrate(app: Application) {
|
||||||
val url = app.environment.config.property("database.url").getString()
|
val url =
|
||||||
val user = app.environment.config.property("database.user").getString()
|
app.environment.config
|
||||||
val password = app.environment.config.property("database.password").getString()
|
.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)
|
log.info("Connecting to {} as {} and running Flyway migrations", url, user)
|
||||||
|
|
||||||
runCatching {
|
runCatching {
|
||||||
Flyway.configure()
|
Flyway
|
||||||
|
.configure()
|
||||||
.dataSource(url, user, password)
|
.dataSource(url, user, password)
|
||||||
.locations("classpath:db/migration")
|
.locations("classpath:db/migration")
|
||||||
.baselineOnMigrate(true)
|
.baselineOnMigrate(true)
|
||||||
|
|||||||
@@ -12,19 +12,19 @@ import kotlin.test.assertEquals
|
|||||||
import kotlin.test.assertTrue
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
class ApplicationTest {
|
class ApplicationTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `health endpoint returns 200 with status ok`() = testApplication {
|
fun `health endpoint returns 200 with status ok`() =
|
||||||
application {
|
testApplication {
|
||||||
install(ContentNegotiation) {
|
application {
|
||||||
json()
|
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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
plugins {
|
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.kotlin.multiplatform")
|
||||||
id("recipe.quality")
|
id("recipe.quality")
|
||||||
alias(libs.plugins.androidLibrary)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
@@ -25,12 +28,18 @@ kotlin {
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "dev.ulfrx.recipe.shared"
|
namespace = "dev.ulfrx.recipe.shared"
|
||||||
compileSdk = libs.versions.android.compileSdk.get().toInt()
|
compileSdk =
|
||||||
|
libs.versions.android.compileSdk
|
||||||
|
.get()
|
||||||
|
.toInt()
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
}
|
}
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdk = libs.versions.android.minSdk.get().toInt()
|
minSdk =
|
||||||
|
libs.versions.android.minSdk
|
||||||
|
.get()
|
||||||
|
.toInt()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ package dev.ulfrx.recipe
|
|||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
|
||||||
class AndroidPlatform : Platform {
|
public class AndroidPlatform : Platform {
|
||||||
override val name: String = "Android ${Build.VERSION.SDK_INT}"
|
override val name: String = "Android ${Build.VERSION.SDK_INT}"
|
||||||
}
|
}
|
||||||
|
|
||||||
actual fun getPlatform(): Platform = AndroidPlatform()
|
public actual fun getPlatform(): Platform = AndroidPlatform()
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
package dev.ulfrx.recipe
|
package dev.ulfrx.recipe
|
||||||
|
|
||||||
const val SERVER_PORT = 8080
|
public const val SERVER_PORT: Int = 8080
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
package dev.ulfrx.recipe
|
package dev.ulfrx.recipe
|
||||||
|
|
||||||
class Greeting {
|
public class Greeting {
|
||||||
private val platform = getPlatform()
|
private val platform = getPlatform()
|
||||||
|
|
||||||
fun greet(): String {
|
public fun greet(): String = "Hello, ${platform.name}!"
|
||||||
return "Hello, ${platform.name}!"
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package dev.ulfrx.recipe
|
package dev.ulfrx.recipe
|
||||||
|
|
||||||
interface Platform {
|
public interface Platform {
|
||||||
val name: String
|
public val name: String
|
||||||
}
|
}
|
||||||
|
|
||||||
expect fun getPlatform(): Platform
|
public expect fun getPlatform(): Platform
|
||||||
|
|||||||
@@ -4,9 +4,8 @@ import kotlin.test.Test
|
|||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
class SharedCommonTest {
|
class SharedCommonTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun example() {
|
fun example() {
|
||||||
assertEquals(3, 1 + 2)
|
assertEquals(3, 1 + 2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ package dev.ulfrx.recipe
|
|||||||
|
|
||||||
import platform.UIKit.UIDevice
|
import platform.UIKit.UIDevice
|
||||||
|
|
||||||
class IOSPlatform : Platform {
|
public class IOSPlatform : Platform {
|
||||||
override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
|
override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
actual fun getPlatform(): Platform = IOSPlatform()
|
public actual fun getPlatform(): Platform = IOSPlatform()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package dev.ulfrx.recipe
|
package dev.ulfrx.recipe
|
||||||
|
|
||||||
class JVMPlatform : Platform {
|
public class JVMPlatform : Platform {
|
||||||
override val name: String = "Java ${System.getProperty("java.version")}"
|
override val name: String = "Java ${System.getProperty("java.version")}"
|
||||||
}
|
}
|
||||||
|
|
||||||
actual fun getPlatform(): Platform = JVMPlatform()
|
public actual fun getPlatform(): Platform = JVMPlatform()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package dev.ulfrx.recipe
|
package dev.ulfrx.recipe
|
||||||
|
|
||||||
class WasmPlatform : Platform {
|
public class WasmPlatform : Platform {
|
||||||
override val name: String = "Web with Kotlin/Wasm"
|
override val name: String = "Web with Kotlin/Wasm"
|
||||||
}
|
}
|
||||||
|
|
||||||
actual fun getPlatform(): Platform = WasmPlatform()
|
public actual fun getPlatform(): Platform = WasmPlatform()
|
||||||
|
|||||||
Reference in New Issue
Block a user