mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 11:18:24 +00:00
chore: share the KMP purity gate, fix stale doc paths and dead imports after the split
Follow-ups from a review of the commons/commonsUI split: - `verifyKmpPurity` was copy-pasted into quartz, commons and commonsUI and the three copies had already drifted (checked dirs, hint text). It now lives once in gradle/kmp-purity.gradle.kts and each module applies it; the checked-dir list is the union, filtered by existence. Verified the shared task still fails on a deliberate java.util.UUID reference in commons. - amethyst/src/main/res/CLAUDE.md and the compose-expert catalog reference still pointed at the pre-split composeResources / ui paths. - Same-package imports left behind in the files moved to commons.feeds and commons.model are removed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N56KzPSYiN5edMRamvKEgD
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
# Shared Composables Catalog
|
||||
|
||||
This catalog documents shared UI components in `commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/`.
|
||||
This catalog documents shared UI components in `commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/` (the Compose half of the shared layer; headless state stays in `commons`).
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
commons/src/commonMain/kotlin/.../commons/ui/
|
||||
commonsUI/src/commonMain/kotlin/.../commons/ui/
|
||||
├── components/ # Reusable UI components
|
||||
├── screens/ # Screen-level composables
|
||||
├── theme/ # Theming and styling
|
||||
@@ -232,7 +232,7 @@ private val pathData1 = PathData {
|
||||
```
|
||||
|
||||
**roboBuilder** - Custom ImageVector.Builder DSL
|
||||
- Located in: `commons/robohash/`
|
||||
- Located in: `commonsUI/.../commons/robohash/`
|
||||
- Pattern: Builder-based, composable paths
|
||||
- Parts: Face, Eyes, Mouth, Body, Accessory (0-9 variants each)
|
||||
- Colors: Dynamic (fgColor parameter) + Black constants
|
||||
|
||||
@@ -43,7 +43,7 @@ It diffs every locale's resource names against its tree's default `values/` and
|
||||
exits non-zero listing any orphan. It covers **both** Crowdin-managed resource
|
||||
systems — the Android res trees (`amethyst/src/main/res`,
|
||||
`commons/src/androidMain/res`) and the Compose-Multiplatform catalog
|
||||
(`commons/src/commonMain/composeResources`) — and says which consequence applies:
|
||||
(`commonsUI/src/commonMain/composeResources`) — and says which consequence applies:
|
||||
lint only polices the Android trees, but an orphan in the Compose catalog is the
|
||||
same mistake and leaves a dead translation behind.
|
||||
|
||||
|
||||
@@ -202,71 +202,5 @@ tasks.withType<Test>().configureEach {
|
||||
}
|
||||
}
|
||||
|
||||
// iOS purity gate — same shape as :quartz:verifyKmpPurity. See the rationale
|
||||
// there. Commons gains this gate once FeedDefinitionSerializer.kt has been
|
||||
// migrated off Jackson; future commonMain code must not reintroduce JVM-only
|
||||
// JSON / HTTP deps.
|
||||
val verifyKmpPurity by tasks.registering {
|
||||
group = "verification"
|
||||
description = "Fails if iOS-targeted source sets import JVM-only deps."
|
||||
val checkedDirs =
|
||||
listOf(
|
||||
"src/commonMain", "src/commonTest",
|
||||
"src/appleMain", "src/appleTest",
|
||||
"src/nativeMain", "src/nativeTest",
|
||||
"src/iosMain", "src/iosTest",
|
||||
"src/iosArm64Main", "src/iosArm64Test",
|
||||
"src/iosSimulatorArm64Main", "src/iosSimulatorArm64Test",
|
||||
"src/linuxMain", "src/linuxTest",
|
||||
"src/linuxX64Main", "src/linuxX64Test",
|
||||
"src/macosMain", "src/macosTest",
|
||||
"src/macosArm64Main", "src/macosArm64Test",
|
||||
).map { layout.projectDirectory.dir(it).asFile }
|
||||
.filter { it.exists() }
|
||||
inputs.files(checkedDirs)
|
||||
doLast {
|
||||
// Each pattern is paired with a short hint so the failure message
|
||||
// points at the canonical KMP replacement.
|
||||
val forbidden =
|
||||
listOf(
|
||||
"com.fasterxml.jackson" to "Jackson is JVM-only — use kotlinx.serialization",
|
||||
"okhttp3" to "OkHttp is JVM-only — wrap behind expect/actual or use Ktor on iOS",
|
||||
"System.currentTimeMillis" to "use TimeUtils.now()",
|
||||
"Thread.sleep" to "use kotlinx.coroutines.delay or platform-specific actual",
|
||||
"java.util.UUID" to "use kotlin.uuid.Uuid",
|
||||
"kotlin.jvm.Synchronized" to "use KmpLock.withLock {}",
|
||||
// The bare call, not just the annotation: `synchronized(lock) {}` resolves
|
||||
// from kotlin-stdlib-jvm with no import, so it compiles on Android/JVM and
|
||||
// only fails at the iOS compile step. Catch it here instead.
|
||||
"synchronized(" to "`synchronized` is JVM-only — use KmpLock.withLock {}",
|
||||
"kotlin.jvm.Volatile" to "use kotlin.concurrent.Volatile",
|
||||
)
|
||||
val offenders =
|
||||
checkedDirs.flatMap { dir ->
|
||||
dir.walkTopDown()
|
||||
.filter { it.isFile && it.extension == "kt" }
|
||||
.flatMap { file ->
|
||||
file.readLines().withIndex().mapNotNull { (idx, line) ->
|
||||
val trimmed = line.trimStart()
|
||||
// Skip KDoc / line-comment lines — those legitimately
|
||||
// mention forbidden names (migration notes, doc refs).
|
||||
if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
forbidden.firstOrNull { (pattern, _) -> line.contains(pattern) }?.let { (hit, hint) ->
|
||||
"${file.relativeTo(rootDir)}:${idx + 1}: '$hit' — $hint"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (offenders.isNotEmpty()) {
|
||||
throw GradleException(
|
||||
"iOS-targeted source sets must not reference JVM-only APIs. " +
|
||||
"Move the offending code to jvmAndroid/ or behind an expect/actual:\n " +
|
||||
offenders.joinToString("\n "),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named("check").configure { dependsOn(verifyKmpPurity) }
|
||||
// iOS purity gate — shared task, see gradle/kmp-purity.gradle.kts.
|
||||
apply(from = rootProject.file("gradle/kmp-purity.gradle.kts"))
|
||||
|
||||
-2
@@ -20,8 +20,6 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.feeds
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.feeds.FeedFilter
|
||||
|
||||
abstract class AdditiveComplexFeedFilter<T, U> : FeedFilter<T>() {
|
||||
abstract fun updateListWith(
|
||||
oldList: List<T>,
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.model
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider
|
||||
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
|
||||
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||
|
||||
-2
@@ -20,8 +20,6 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.model
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||
|
||||
-2
@@ -20,8 +20,6 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.model
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.User
|
||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
@@ -20,10 +20,6 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.model
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.commons.model.Channel
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.User
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.ICacheEventStream
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
|
||||
@@ -224,70 +224,5 @@ compose.resources {
|
||||
generateResClass = always
|
||||
}
|
||||
|
||||
// iOS purity gate — same shape as :quartz / :commons verifyKmpPurity.
|
||||
// commonMain here must stay free of JVM-only JSON / HTTP deps.
|
||||
val verifyKmpPurity by tasks.registering {
|
||||
group = "verification"
|
||||
description = "Fails if iOS-targeted source sets import JVM-only deps."
|
||||
val checkedDirs =
|
||||
listOf(
|
||||
"src/commonMain", "src/commonTest",
|
||||
"src/appleMain", "src/appleTest",
|
||||
"src/nativeMain", "src/nativeTest",
|
||||
"src/iosMain", "src/iosTest",
|
||||
"src/skikoMain", "src/skikoTest",
|
||||
"src/iosArm64Main", "src/iosArm64Test",
|
||||
"src/iosSimulatorArm64Main", "src/iosSimulatorArm64Test",
|
||||
"src/linuxMain", "src/linuxTest",
|
||||
"src/linuxX64Main", "src/linuxX64Test",
|
||||
"src/macosMain", "src/macosTest",
|
||||
"src/macosArm64Main", "src/macosArm64Test",
|
||||
).map { layout.projectDirectory.dir(it).asFile }
|
||||
.filter { it.exists() }
|
||||
inputs.files(checkedDirs)
|
||||
doLast {
|
||||
// Each pattern is paired with a short hint so the failure message
|
||||
// points at the canonical KMP replacement.
|
||||
val forbidden =
|
||||
listOf(
|
||||
"com.fasterxml.jackson" to "Jackson is JVM-only — use kotlinx.serialization",
|
||||
"okhttp3" to "OkHttp is JVM-only — wrap behind expect/actual or use Ktor on iOS",
|
||||
"System.currentTimeMillis" to "use TimeUtils.now()",
|
||||
"Thread.sleep" to "use kotlinx.coroutines.delay or platform-specific actual",
|
||||
"java.util.UUID" to "use kotlin.uuid.Uuid",
|
||||
"kotlin.jvm.Synchronized" to "use KmpLock.withLock {}",
|
||||
// The bare call, not just the annotation: `synchronized(lock) {}` resolves
|
||||
// from kotlin-stdlib-jvm with no import, so it compiles on Android/JVM and
|
||||
// only fails at the iOS compile step. Catch it here instead.
|
||||
"synchronized(" to "`synchronized` is JVM-only — use KmpLock.withLock {}",
|
||||
"kotlin.jvm.Volatile" to "use kotlin.concurrent.Volatile",
|
||||
)
|
||||
val offenders =
|
||||
checkedDirs.flatMap { dir ->
|
||||
dir.walkTopDown()
|
||||
.filter { it.isFile && it.extension == "kt" }
|
||||
.flatMap { file ->
|
||||
file.readLines().withIndex().mapNotNull { (idx, line) ->
|
||||
val trimmed = line.trimStart()
|
||||
// Skip KDoc / line-comment lines — those legitimately
|
||||
// mention forbidden names (migration notes, doc refs).
|
||||
if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
forbidden.firstOrNull { (pattern, _) -> line.contains(pattern) }?.let { (hit, hint) ->
|
||||
"${file.relativeTo(rootDir)}:${idx + 1}: '$hit' — $hint"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (offenders.isNotEmpty()) {
|
||||
throw GradleException(
|
||||
"iOS-targeted source sets must not reference JVM-only APIs. " +
|
||||
"Move the offending code to jvmAndroid/ or behind an expect/actual:\n " +
|
||||
offenders.joinToString("\n "),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named("check").configure { dependsOn(verifyKmpPurity) }
|
||||
// iOS purity gate — shared task, see gradle/kmp-purity.gradle.kts.
|
||||
apply(from = rootProject.file("gradle/kmp-purity.gradle.kts"))
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
// iOS purity gate shared by every KMP library module that targets Apple
|
||||
// (`:quartz`, `:commons`, `:commonsUI`). Apply it from the module's build
|
||||
// script with
|
||||
//
|
||||
// apply(from = rootProject.file("gradle/kmp-purity.gradle.kts"))
|
||||
//
|
||||
// It registers `verifyKmpPurity` (wired into `check`) which fails when an
|
||||
// iOS-targeted source set references a JVM-only API. `verifyKmpPurity` is the
|
||||
// fast Linux pre-check that runs in the lint job; the macOS iOS compile job is
|
||||
// the authoritative one. Keeping the pattern table in one file means a new
|
||||
// forbidden API lands in every module at once instead of drifting per copy.
|
||||
|
||||
val verifyKmpPurity by tasks.registering {
|
||||
group = "verification"
|
||||
description = "Fails if iOS-targeted source sets import JVM-only deps."
|
||||
// Every source set that feeds an Apple target. Modules that don't have a
|
||||
// given directory are skipped by the exists() filter, so the list is the
|
||||
// union across modules (e.g. only :commonsUI has skikoMain today).
|
||||
val checkedDirs =
|
||||
listOf(
|
||||
"src/commonMain", "src/commonTest",
|
||||
"src/appleMain", "src/appleTest",
|
||||
"src/nativeMain", "src/nativeTest",
|
||||
"src/iosMain", "src/iosTest",
|
||||
"src/skikoMain", "src/skikoTest",
|
||||
"src/iosArm64Main", "src/iosArm64Test",
|
||||
"src/iosSimulatorArm64Main", "src/iosSimulatorArm64Test",
|
||||
"src/linuxMain", "src/linuxTest",
|
||||
"src/linuxX64Main", "src/linuxX64Test",
|
||||
"src/macosMain", "src/macosTest",
|
||||
"src/macosArm64Main", "src/macosArm64Test",
|
||||
).map { layout.projectDirectory.dir(it).asFile }
|
||||
.filter { it.exists() }
|
||||
inputs.files(checkedDirs)
|
||||
doLast {
|
||||
// Each pattern is paired with a short hint so the failure message
|
||||
// points at the canonical KMP replacement.
|
||||
val forbidden =
|
||||
listOf(
|
||||
"com.fasterxml.jackson" to "Jackson is JVM-only — use kotlinx.serialization",
|
||||
"okhttp3" to "OkHttp is JVM-only — wrap behind expect/actual or use Ktor on iOS",
|
||||
"System.currentTimeMillis" to "use TimeUtils.now()",
|
||||
"Thread.sleep" to "use kotlinx.coroutines.delay or platform-specific actual",
|
||||
"java.util.UUID" to "use kotlin.uuid.Uuid",
|
||||
"kotlin.jvm.Synchronized" to "use a KMP lock primitive (KmpLock.withLock {} in commons)",
|
||||
// The bare call, not just the annotation: `synchronized(lock) {}` resolves
|
||||
// from kotlin-stdlib-jvm with no import, so it compiles on Android/JVM and
|
||||
// only fails at the iOS compile step. Catch it here instead.
|
||||
"synchronized(" to "`synchronized` is JVM-only — use a KMP lock primitive (KmpLock.withLock {} in commons)",
|
||||
"kotlin.jvm.Volatile" to "use kotlin.concurrent.Volatile",
|
||||
)
|
||||
val offenders =
|
||||
checkedDirs.flatMap { dir ->
|
||||
dir
|
||||
.walkTopDown()
|
||||
.filter { it.isFile && it.extension == "kt" }
|
||||
.flatMap { file ->
|
||||
file.readLines().withIndex().mapNotNull { (idx, line) ->
|
||||
val trimmed = line.trimStart()
|
||||
// Skip KDoc / line-comment lines — those legitimately
|
||||
// mention forbidden names (migration notes, doc refs).
|
||||
if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
forbidden.firstOrNull { (pattern, _) -> line.contains(pattern) }?.let { (hit, hint) ->
|
||||
"${file.relativeTo(rootDir)}:${idx + 1}: '$hit' — $hint"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (offenders.isNotEmpty()) {
|
||||
throw GradleException(
|
||||
"iOS-targeted source sets must not reference JVM-only APIs. " +
|
||||
"Move the offending code to jvmAndroid/ or behind an expect/actual:\n " +
|
||||
offenders.joinToString("\n "),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named("check").configure { dependsOn(verifyKmpPurity) }
|
||||
+4
-73
@@ -383,79 +383,10 @@ dependencies {
|
||||
// Scope: source sets whose code is compiled for at least one non-JVM
|
||||
// target. Excludes jvmAndroid, jvmMain, androidMain (and their tests),
|
||||
// where Jackson and OkHttp are legitimately used.
|
||||
val verifyKmpPurity by tasks.registering {
|
||||
group = "verification"
|
||||
description = "Fails if iOS-targeted source sets import JVM-only deps."
|
||||
val checkedDirs =
|
||||
listOf(
|
||||
"src/commonMain",
|
||||
"src/commonTest",
|
||||
"src/appleMain",
|
||||
"src/appleTest",
|
||||
"src/nativeMain",
|
||||
"src/nativeTest",
|
||||
"src/iosMain",
|
||||
"src/iosTest",
|
||||
"src/iosArm64Main",
|
||||
"src/iosArm64Test",
|
||||
"src/iosSimulatorArm64Main",
|
||||
"src/iosSimulatorArm64Test",
|
||||
"src/linuxMain",
|
||||
"src/linuxTest",
|
||||
"src/linuxX64Main",
|
||||
"src/linuxX64Test",
|
||||
"src/macosMain",
|
||||
"src/macosTest",
|
||||
"src/macosArm64Main",
|
||||
"src/macosArm64Test",
|
||||
).map { layout.projectDirectory.dir(it).asFile }
|
||||
.filter { it.exists() }
|
||||
inputs.files(checkedDirs)
|
||||
doLast {
|
||||
// Each pattern is paired with a short hint so the failure message
|
||||
// points at the canonical KMP replacement.
|
||||
val forbidden =
|
||||
listOf(
|
||||
"com.fasterxml.jackson" to "Jackson is JVM-only — use kotlinx.serialization",
|
||||
"okhttp3" to "OkHttp is JVM-only — wrap behind expect/actual or use Ktor on iOS",
|
||||
"System.currentTimeMillis" to "use TimeUtils.now()",
|
||||
"Thread.sleep" to "use kotlinx.coroutines.delay or platform-specific actual",
|
||||
"java.util.UUID" to "use kotlin.uuid.Uuid",
|
||||
"kotlin.jvm.Synchronized" to "use a KMP lock primitive",
|
||||
// The bare call, not just the annotation: `synchronized(lock) {}` resolves
|
||||
// from kotlin-stdlib-jvm with no import, so it compiles on Android/JVM and
|
||||
// only fails at the iOS compile step. Catch it here instead.
|
||||
"synchronized(" to "`synchronized` is JVM-only — use a KMP lock primitive",
|
||||
"kotlin.jvm.Volatile" to "use kotlin.concurrent.Volatile",
|
||||
)
|
||||
val offenders =
|
||||
checkedDirs.flatMap { dir ->
|
||||
dir
|
||||
.walkTopDown()
|
||||
.filter { it.isFile && it.extension == "kt" }
|
||||
.flatMap { file ->
|
||||
file.readLines().withIndex().mapNotNull { (idx, line) ->
|
||||
val trimmed = line.trimStart()
|
||||
if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
forbidden.firstOrNull { (pattern, _) -> line.contains(pattern) }?.let { (hit, hint) ->
|
||||
"${file.relativeTo(rootDir)}:${idx + 1}: '$hit' — $hint"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (offenders.isNotEmpty()) {
|
||||
throw GradleException(
|
||||
"iOS-targeted source sets must not reference JVM-only APIs. " +
|
||||
"Move the offending code to jvmAndroid/ or behind an expect/actual:\n " +
|
||||
offenders.joinToString("\n "),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named("check").configure { dependsOn(verifyKmpPurity) }
|
||||
//
|
||||
// The task itself is shared with :commons and :commonsUI — see
|
||||
// gradle/kmp-purity.gradle.kts for the pattern table and checked dirs.
|
||||
apply(from = rootProject.file("gradle/kmp-purity.gradle.kts"))
|
||||
|
||||
mavenPublishing {
|
||||
// sources publishing is always enabled by the Kotlin Multiplatform plugin
|
||||
|
||||
Reference in New Issue
Block a user