feat: add runtime configuration for Caddy-based SPA containerization
Introduced `config.json` runtime configuration fetch mechanism to support the "Build Once, Deploy Everywhere" pattern. Replaced NGINX with Caddy for SPA deployment, enabling SPA routing, security headers, and static asset management. Updated Gradle and Kotlin/JS build configurations to align with the new runtime environment. Enhanced Dockerfile and health checks for optimized CI/CD workflows and improved SPA delivery.
This commit is contained in:
@@ -14,11 +14,15 @@ kotlin {
|
||||
jvm()
|
||||
|
||||
js {
|
||||
browser {
|
||||
testTask {
|
||||
enabled = false
|
||||
}
|
||||
// browser {} block removed to avoid NodeJsRootPlugin conflicts in multi-module builds
|
||||
// We only need explicit browser configuration in the shell (application) module.
|
||||
// Tests are disabled via root build.gradle.kts configuration anyway.
|
||||
nodejs {
|
||||
testTask {
|
||||
enabled = false
|
||||
}
|
||||
}
|
||||
binaries.library()
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
|
||||
@@ -6,48 +6,27 @@ plugins {
|
||||
}
|
||||
|
||||
kotlin {
|
||||
// Toolchain is now handled centrally in the root build.gradle.kts
|
||||
|
||||
jvm()
|
||||
js(IR) {
|
||||
browser()
|
||||
// nodejs()
|
||||
}
|
||||
|
||||
// Wasm vorerst deaktiviert
|
||||
/*
|
||||
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
|
||||
wasmJs {
|
||||
browser()
|
||||
js(IR) {
|
||||
binaries.library()
|
||||
// Explicitly select browser environment to satisfy Kotlin/JS compiler warning
|
||||
browser {
|
||||
testTask { enabled = false }
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
|
||||
// Compose dependencies
|
||||
implementation(compose.runtime)
|
||||
implementation(compose.foundation)
|
||||
implementation(compose.material3)
|
||||
implementation(compose.ui)
|
||||
implementation(compose.components.resources)
|
||||
|
||||
// Coroutines
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
|
||||
// Serialization
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
|
||||
// DateTime
|
||||
implementation(libs.kotlinx.datetime)
|
||||
}
|
||||
|
||||
jsMain.dependencies {
|
||||
// JS-specific UI dependencies if needed
|
||||
}
|
||||
|
||||
jvmMain.dependencies {
|
||||
// JVM-specific UI dependencies if needed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,24 +9,14 @@ plugins {
|
||||
}
|
||||
|
||||
kotlin {
|
||||
// Toolchain is now handled centrally in the root build.gradle.kts
|
||||
|
||||
// Wasm is now a first-class citizen in our stack, so we enable it by default
|
||||
// val enableWasm = providers.gradleProperty("enableWasm").orNull == "true"
|
||||
|
||||
jvm()
|
||||
js {
|
||||
binaries.library()
|
||||
browser {
|
||||
testTask { enabled = false }
|
||||
testTask { enabled = false }
|
||||
}
|
||||
}
|
||||
|
||||
// Wasm vorerst deaktiviert
|
||||
/*
|
||||
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
|
||||
wasmJs { browser() }
|
||||
*/
|
||||
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
|
||||
@@ -9,28 +9,18 @@ plugins {
|
||||
}
|
||||
|
||||
kotlin {
|
||||
// Toolchain is now handled centrally in the root build.gradle.kts
|
||||
|
||||
jvm()
|
||||
js {
|
||||
binaries.library()
|
||||
browser {
|
||||
testTask { enabled = false }
|
||||
testTask { enabled = false }
|
||||
}
|
||||
binaries.executable()
|
||||
}
|
||||
|
||||
// Wasm vorerst deaktiviert, um Stabilität mit JS zu gewährleisten
|
||||
/*
|
||||
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
|
||||
wasmJs {
|
||||
browser()
|
||||
}
|
||||
*/
|
||||
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
implementation(libs.koin.core)
|
||||
implementation(libs.bundles.kmp.common) // Coroutines, Serialization, DateTime
|
||||
implementation(libs.bundles.kmp.common)
|
||||
implementation(libs.sqldelight.runtime)
|
||||
implementation(libs.sqldelight.coroutines)
|
||||
}
|
||||
@@ -41,18 +31,9 @@ kotlin {
|
||||
|
||||
jsMain.dependencies {
|
||||
implementation(libs.sqldelight.driver.web)
|
||||
|
||||
// NPM deps used by `sqlite.worker.js` (OPFS-backed SQLite WASM worker)
|
||||
implementation(npm("@sqlite.org/sqlite-wasm", "3.51.1-build2"))
|
||||
}
|
||||
|
||||
/*
|
||||
val wasmJsMain = getByName("wasmJsMain")
|
||||
wasmJsMain.dependencies {
|
||||
implementation(libs.sqldelight.driver.web)
|
||||
}
|
||||
*/
|
||||
|
||||
commonTest.dependencies {
|
||||
implementation(libs.kotlin.test)
|
||||
}
|
||||
@@ -63,7 +44,7 @@ sqldelight {
|
||||
databases {
|
||||
create("AppDatabase") {
|
||||
packageName.set("at.mocode.frontend.core.localdb")
|
||||
generateAsync.set(true) // WICHTIG: Async-First für JS Support
|
||||
generateAsync.set(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,28 +10,14 @@ group = "at.mocode.clients.shared"
|
||||
version = "1.0.0"
|
||||
|
||||
kotlin {
|
||||
// Toolchain is now handled centrally in the root build.gradle.kts
|
||||
|
||||
jvm()
|
||||
|
||||
js {
|
||||
binaries.library()
|
||||
browser {
|
||||
testTask {
|
||||
// Browser testing is disabled to avoid environment issues (e.g. missing ChromeHeadless).
|
||||
// Tests are still run on JVM.
|
||||
enabled = false
|
||||
}
|
||||
testTask { enabled = false }
|
||||
}
|
||||
}
|
||||
|
||||
// Wasm vorerst deaktiviert
|
||||
/*
|
||||
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
|
||||
wasmJs {
|
||||
browser()
|
||||
}
|
||||
*/
|
||||
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
// Depend on core domain for User/Role types used by navigation API
|
||||
|
||||
@@ -9,39 +9,23 @@ plugins {
|
||||
}
|
||||
|
||||
kotlin {
|
||||
// Toolchain is now handled centrally in the root build.gradle.kts
|
||||
|
||||
jvm()
|
||||
js {
|
||||
binaries.library()
|
||||
browser {
|
||||
testTask { enabled = false }
|
||||
testTask { enabled = false }
|
||||
}
|
||||
}
|
||||
|
||||
// Wasm vorerst deaktiviert
|
||||
/*
|
||||
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
|
||||
wasmJs { browser() }
|
||||
*/
|
||||
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
// Ktor Client core + JSON and Auth + Logging + Timeouts + Retry
|
||||
api(libs.ktor.client.core)
|
||||
implementation(libs.ktor.client.contentNegotiation)
|
||||
implementation(libs.ktor.client.serialization.kotlinx.json)
|
||||
implementation(libs.ktor.client.auth)
|
||||
implementation(libs.ktor.client.logging)
|
||||
// ktor-client-resources optional; disabled until version is added to catalog
|
||||
|
||||
// Kotlinx core bundles
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
|
||||
// DI (Koin)
|
||||
api(libs.koin.core)
|
||||
|
||||
// Project modules via typesafe accessors
|
||||
// (none here; kept for consistency)
|
||||
}
|
||||
|
||||
jvmMain.dependencies {
|
||||
@@ -51,13 +35,6 @@ kotlin {
|
||||
jsMain.dependencies {
|
||||
implementation(libs.ktor.client.js)
|
||||
}
|
||||
|
||||
/*
|
||||
val wasmJsMain = getByName("wasmJsMain")
|
||||
wasmJsMain.dependencies {
|
||||
implementation(libs.ktor.client.js)
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,15 +4,20 @@ plugins {
|
||||
}
|
||||
|
||||
kotlin {
|
||||
// Targets are configured centrally in the shells/feature modules; here we just provide common code.
|
||||
jvm()
|
||||
js(IR) {
|
||||
browser()
|
||||
binaries.library()
|
||||
browser {
|
||||
testTask { enabled = false }
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
// Correct dependency: Syncable interface is in the shared core domain
|
||||
implementation(projects.core.coreDomain)
|
||||
// Also include frontend domain if needed (e.g., for frontend-specific models)
|
||||
implementation(projects.frontend.core.domain)
|
||||
|
||||
// Networking
|
||||
implementation(libs.ktor.client.core)
|
||||
|
||||
@@ -2,8 +2,6 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
/**
|
||||
* Dieses Modul kapselt die gesamte UI und Logik für das Ping-Feature.
|
||||
* Es kennt seine eigenen technischen Abhängigkeiten (Ktor, Coroutines)
|
||||
* und den UI-Baukasten (common-ui), aber es kennt keine anderen Features.
|
||||
*/
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
@@ -16,45 +14,23 @@ group = "at.mocode.clients"
|
||||
version = "1.0.0"
|
||||
|
||||
kotlin {
|
||||
// Toolchain is now handled centrally in the root build.gradle.kts
|
||||
|
||||
jvm()
|
||||
|
||||
js {
|
||||
binaries.library()
|
||||
browser {
|
||||
testTask {
|
||||
enabled = false
|
||||
}
|
||||
testTask { enabled = false }
|
||||
}
|
||||
}
|
||||
|
||||
// Wasm vorerst deaktiviert
|
||||
/*
|
||||
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
|
||||
wasmJs {
|
||||
browser()
|
||||
}
|
||||
*/
|
||||
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
// Contract from backend
|
||||
implementation(projects.contracts.pingApi)
|
||||
|
||||
// UI Kit (Design System)
|
||||
implementation(projects.frontend.core.designSystem)
|
||||
|
||||
// Generic Delta-Sync core
|
||||
implementation(projects.frontend.core.sync)
|
||||
|
||||
// Local DB (SQLDelight)
|
||||
implementation(projects.frontend.core.localDb)
|
||||
implementation(libs.sqldelight.coroutines) // Explicitly add coroutines extension for async driver support
|
||||
implementation(libs.sqldelight.coroutines)
|
||||
implementation(projects.frontend.core.domain)
|
||||
|
||||
// Shared sync contract base (Syncable)
|
||||
implementation(projects.core.coreDomain)
|
||||
|
||||
// Compose dependencies
|
||||
implementation(compose.foundation)
|
||||
implementation(compose.runtime)
|
||||
implementation(compose.material3)
|
||||
@@ -62,12 +38,10 @@ kotlin {
|
||||
implementation(compose.components.resources)
|
||||
implementation(compose.materialIconsExtended)
|
||||
|
||||
// Bundles (Cleaned up dependencies)
|
||||
implementation(libs.bundles.kmp.common) // Coroutines, Serialization, DateTime
|
||||
implementation(libs.bundles.ktor.client.common) // Ktor Client (Core, Auth, JSON, Logging)
|
||||
implementation(libs.bundles.compose.common) // ViewModel & Lifecycle
|
||||
implementation(libs.bundles.kmp.common)
|
||||
implementation(libs.bundles.ktor.client.common)
|
||||
implementation(libs.bundles.compose.common)
|
||||
|
||||
// DI (Koin) for resolving apiClient from container
|
||||
implementation(libs.koin.core)
|
||||
}
|
||||
|
||||
@@ -78,7 +52,7 @@ kotlin {
|
||||
}
|
||||
|
||||
jvmTest.dependencies {
|
||||
implementation(libs.mockk) // MockK only for JVM tests
|
||||
implementation(libs.mockk)
|
||||
implementation(projects.platform.platformTesting)
|
||||
implementation(libs.bundles.testing.jvm)
|
||||
}
|
||||
@@ -90,22 +64,9 @@ kotlin {
|
||||
jsMain.dependencies {
|
||||
implementation(libs.ktor.client.js)
|
||||
}
|
||||
|
||||
/*
|
||||
val wasmJsMain = getByName("wasmJsMain")
|
||||
wasmJsMain.dependencies {
|
||||
implementation(libs.ktor.client.js) // WASM verwendet JS-Client [cite: 7]
|
||||
|
||||
// Compose für shared UI components für WASM
|
||||
implementation(compose.runtime)
|
||||
implementation(compose.foundation)
|
||||
implementation(compose.material3)
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
// KMP Compile-Optionen
|
||||
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_25)
|
||||
|
||||
+2
-2
@@ -132,8 +132,8 @@ class TestPingApiClient : PingApi {
|
||||
return handleRequest(securePingResponse)
|
||||
}
|
||||
|
||||
override suspend fun syncPings(lastSyncTimestamp: Long): List<PingEvent> {
|
||||
syncPingsCalledWith = lastSyncTimestamp
|
||||
override suspend fun syncPings(since: Long): List<PingEvent> {
|
||||
syncPingsCalledWith = since
|
||||
callCount++
|
||||
|
||||
if (simulateDelay) {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import kotlinx.browser.window
|
||||
import kotlinx.coroutines.await
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
@Serializable
|
||||
data class AppConfig(
|
||||
val apiBaseUrl: String
|
||||
)
|
||||
|
||||
suspend fun loadAppConfig(): AppConfig {
|
||||
return try {
|
||||
// Fetch config.json generated by Caddy templates
|
||||
val response = window.fetch("/config.json").await()
|
||||
if (!response.ok) {
|
||||
console.warn("[Config] Failed to load config.json, falling back to defaults")
|
||||
return AppConfig(apiBaseUrl = window.location.origin)
|
||||
}
|
||||
val text = response.text().await()
|
||||
Json.decodeFromString(AppConfig.serializer(), text)
|
||||
} catch (e: dynamic) {
|
||||
console.error("[Config] Error loading config:", e)
|
||||
// Fallback for local development if file is missing
|
||||
AppConfig(apiBaseUrl = "http://localhost:8081")
|
||||
}
|
||||
}
|
||||
@@ -21,20 +21,30 @@ import org.w3c.dom.HTMLElement
|
||||
fun main() {
|
||||
console.log("[WebApp] main() entered")
|
||||
|
||||
// 1. Initialize DI (Koin) with static modules
|
||||
try {
|
||||
startKoin { modules(networkModule, localDbModule, syncModule, pingFeatureModule, authModule, navigationModule) }
|
||||
console.log("[WebApp] Koin initialized with static modules")
|
||||
} catch (e: dynamic) {
|
||||
console.warn("[WebApp] Koin initialization warning:", e)
|
||||
}
|
||||
|
||||
// 2. Async Initialization Chain
|
||||
// We must ensure DB is ready and registered in Koin BEFORE we mount the UI.
|
||||
val provider = GlobalContext.get().get<DatabaseProvider>()
|
||||
|
||||
MainScope().launch {
|
||||
try {
|
||||
// 1. Load Runtime Configuration (Async)
|
||||
console.log("[WebApp] Loading configuration...")
|
||||
val config = loadAppConfig()
|
||||
console.log("[WebApp] Configuration loaded: apiBaseUrl=${config.apiBaseUrl}")
|
||||
|
||||
// 2. Initialize DI (Koin)
|
||||
// We register the config immediately so other modules can use it
|
||||
startKoin {
|
||||
modules(
|
||||
module { single { config } }, // Make AppConfig available for injection
|
||||
networkModule,
|
||||
localDbModule,
|
||||
syncModule,
|
||||
pingFeatureModule,
|
||||
authModule,
|
||||
navigationModule
|
||||
)
|
||||
}
|
||||
console.log("[WebApp] Koin initialized")
|
||||
|
||||
// 3. Initialize Database (Async)
|
||||
val provider = GlobalContext.get().get<DatabaseProvider>()
|
||||
console.log("[WebApp] Initializing Database...")
|
||||
val db = provider.createDatabase()
|
||||
|
||||
@@ -46,12 +56,12 @@ fun main() {
|
||||
)
|
||||
console.log("[WebApp] Local DB created and registered in Koin")
|
||||
|
||||
// 3. Start App only after DB is ready
|
||||
// 4. Start UI
|
||||
startAppWhenDomReady()
|
||||
|
||||
} catch (e: dynamic) {
|
||||
console.error("[WebApp] CRITICAL: Database initialization failed:", e)
|
||||
renderFatalError("Database initialization failed: ${e?.message ?: e}")
|
||||
console.error("[WebApp] CRITICAL: Initialization failed:", e)
|
||||
renderFatalError("Initialization failed: ${e?.message ?: e}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user