feat(MP-29): navigation core module, auth guards & shell wiring\n\n- Establish :frontend:core:navigation module with DeepLinkHandler\n- Introduce NavigationPort & CurrentUserProvider (DI)\n- Harden admin routes against AppRoles.ADMIN\n- Wire Koin in JS/JVM/Wasm shells (navigationModule)\n- Remove legacy DeepLinkHandler from shared\n- Add unit tests for guard logic\n\nRef: MP-29 (#24)

This commit is contained in:
StefanMo
2025-12-08 14:23:08 +01:00
committed by GitHub
parent df2562ea23
commit 5ea4730cd4
14 changed files with 283 additions and 236 deletions
+43 -35
View File
@@ -222,14 +222,15 @@ tasks.register("archGuardNoFeatureToFeatureDeps") {
.forEach { cfg ->
cfg.dependencies.withType(org.gradle.api.artifacts.ProjectDependency::class.java).forEach { dep ->
// Use reflection to avoid compile-time issues with dependencyProject property
val proj = try {
dep.javaClass.getMethod("getDependencyProject").invoke(dep) as org.gradle.api.Project
} catch (e: Throwable) {
null
}
val proj =
try {
dep.javaClass.getMethod("getDependencyProject").invoke(dep) as org.gradle.api.Project
} catch (e: Throwable) {
null
}
val target = proj?.path ?: ""
if (target.startsWith(featurePrefix) && target != p.path) {
violations += "${p.path} -> ${target} (configuration: ${cfg.name})"
violations += "${p.path} -> $target (configuration: ${cfg.name})"
}
}
}
@@ -237,12 +238,13 @@ tasks.register("archGuardNoFeatureToFeatureDeps") {
}
if (violations.isNotEmpty()) {
val msg = buildString {
appendLine("Feature isolation violation(s) detected:")
violations.forEach { appendLine(" - $it") }
appendLine()
appendLine("Policy: frontend features must not depend on other features. Use navigation/shared domain in :frontend:core instead.")
}
val msg =
buildString {
appendLine("Feature isolation violation(s) detected:")
violations.forEach { appendLine(" - $it") }
appendLine()
appendLine("Policy: frontend features must not depend on other features. Use navigation/shared domain in :frontend:core instead.")
}
throw GradleException(msg)
}
}
@@ -251,12 +253,13 @@ tasks.register("archGuardNoFeatureToFeatureDeps") {
// ------------------------------------------------------------------
// Bundle Size Budgets for Frontend Shells (Kotlin/JS)
// ------------------------------------------------------------------
// ✅ FIX: Klasse auf Top-Level verschieben
data class Budget(val rawBytes: Long, val gzipBytes: Long)
tasks.register("checkBundleBudget") {
group = "verification"
description = "Checks JS bundle sizes of frontend shells against configured budgets"
doLast {
data class Budget(val rawBytes: Long, val gzipBytes: Long)
val budgetsFile = file("config/bundles/budgets.json")
if (!budgetsFile.exists()) {
throw GradleException("Budgets file not found: ${budgetsFile.path}")
@@ -264,14 +267,16 @@ tasks.register("checkBundleBudget") {
// Load budgets JSON as simple Map<String, Budget>
val text = budgetsFile.readText()
@Suppress("UNCHECKED_CAST")
val parsed =
groovy.json.JsonSlurper().parseText(text) as Map<String, Map<String, Any?>>
val budgets = parsed.mapValues { (_, v) ->
val raw = (v["rawBytes"] as Number).toLong()
val gz = (v["gzipBytes"] as Number).toLong()
Budget(raw, gz)
}
val budgets =
parsed.mapValues { (_, v) ->
val raw = (v["rawBytes"] as Number).toLong()
val gz = (v["gzipBytes"] as Number).toLong()
Budget(raw, gz)
}
fun gzipSize(bytes: ByteArray): Long {
val baos = java.io.ByteArrayOutputStream()
@@ -293,12 +298,14 @@ tasks.register("checkBundleBudget") {
shells.forEach { shell ->
val key = shell.path.trimStart(':').replace(':', '/') // or use colon form for budgets keys below
val colonKey = shell.path.trimStart(':').replace('/', ':').trim() // ensure ":a:b:c"
// Budgets are keyed by Gradle path with colons but without leading colon in config for readability
val budgetKeyCandidates = listOf(
shell.path.removePrefix(":"), // e.g., frontend:shells:meldestelle-portal
colonKey.removePrefix(":"),
shell.name,
)
// Budgets are keyed by a Gradle path with colons but without leading colon in config for readability
val budgetKeyCandidates =
listOf(
// e.g., frontend:shells:meldestelle-portal
shell.path.removePrefix(":"),
colonKey.removePrefix(":"),
shell.name,
)
val budgetEntry = budgetKeyCandidates.asSequence().mapNotNull { budgets[it] }.firstOrNull()
if (budgetEntry == null) {
@@ -334,13 +341,13 @@ tasks.register("checkBundleBudget") {
val top = topFiles.sortedByDescending { it.second }.take(5)
report.appendLine("- ${shell.path}:")
report.appendLine(" raw: ${totalRaw} bytes (budget ${budgetEntry.rawBytes})")
report.appendLine(" gzip: ${totalGzip} bytes (budget ${budgetEntry.gzipBytes})")
report.appendLine(" raw: $totalRaw bytes (budget ${budgetEntry.rawBytes})")
report.appendLine(" gzip: $totalGzip bytes (budget ${budgetEntry.gzipBytes})")
report.appendLine(" top files:")
top.forEach { (n, s) -> report.appendLine(" - $n: ${s} bytes") }
top.forEach { (n, s) -> report.appendLine(" - $n: $s bytes") }
if (totalRaw > budgetEntry.rawBytes || totalGzip > budgetEntry.gzipBytes) {
errors += "${shell.path}: raw=${totalRaw}/${budgetEntry.rawBytes}, gzip=${totalGzip}/${budgetEntry.gzipBytes}"
errors += "${shell.path}: raw=$totalRaw/${budgetEntry.rawBytes}, gzip=$totalGzip/${budgetEntry.gzipBytes}"
}
}
@@ -349,12 +356,13 @@ tasks.register("checkBundleBudget") {
file(outDir.resolve("bundle-budgets.txt")).writeText(report.toString())
if (errors.isNotEmpty()) {
val msg = buildString {
appendLine("Bundle budget violations:")
errors.forEach { appendLine(" - $it") }
appendLine()
appendLine("See report: ${outDir.resolve("bundle-budgets.txt").path}")
}
val msg =
buildString {
appendLine("Bundle budget violations:")
errors.forEach { appendLine(" - $it") }
appendLine()
appendLine("See report: ${outDir.resolve("bundle-budgets.txt").path}")
}
throw GradleException(msg)
} else {
println(report.toString())