From 13832c6246076f2f4a7c353ef61708d035fd63f8 Mon Sep 17 00:00:00 2001 From: Isaac Udy Date: Tue, 26 May 2026 00:41:00 +1200 Subject: [PATCH] Fix Kotlin/JS publish: expect/actual for MetadataKey identifier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit enro-common targets Kotlin/JS (for non-UI KMP scenarios like a NodeJS backend), and Kotlin/JS reflection deliberately refuses qualifiedName at compile time. NavigationKey.MetadataKey.name was using this::class.qualifiedName from commonMain, which compiled on every other target but stopped publishToMavenLocal in its tracks with: e: …/NavigationKey.kt:227:25 This reflection API is not supported in Kotlin/JS. The reflection call is now an internal expect fun metadataKeyName(kClass) in commonMain, with actuals per source set: - androidMain / desktopMain / iosMain / wasmJsMain — qualifiedName, with a simpleName fallback for the defensive case. - jsMain — simpleName only (with a doc comment explaining the cross-package collision risk and why the common `object MyKey : MetadataKey<…>` pattern stays safe). Verified `./gradlew :enro-common:publishToMavenLocal` succeeds on all five targets, and `:enro-runtime:desktopTest` still green. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../dev/enro/metadataKeyName.android.kt | 9 ++++++++ .../kotlin/dev/enro/NavigationKey.kt | 2 +- .../kotlin/dev/enro/metadataKeyName.kt | 21 +++++++++++++++++++ .../dev/enro/metadataKeyName.desktop.kt | 9 ++++++++ .../kotlin/dev/enro/metadataKeyName.ios.kt | 9 ++++++++ .../kotlin/dev/enro/metadataKeyName.js.kt | 12 +++++++++++ .../kotlin/dev/enro/metadataKeyName.wasmJs.kt | 9 ++++++++ 7 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 enro-common/src/androidMain/kotlin/dev/enro/metadataKeyName.android.kt create mode 100644 enro-common/src/commonMain/kotlin/dev/enro/metadataKeyName.kt create mode 100644 enro-common/src/desktopMain/kotlin/dev/enro/metadataKeyName.desktop.kt create mode 100644 enro-common/src/iosMain/kotlin/dev/enro/metadataKeyName.ios.kt create mode 100644 enro-common/src/jsMain/kotlin/dev/enro/metadataKeyName.js.kt create mode 100644 enro-common/src/wasmJsMain/kotlin/dev/enro/metadataKeyName.wasmJs.kt diff --git a/enro-common/src/androidMain/kotlin/dev/enro/metadataKeyName.android.kt b/enro-common/src/androidMain/kotlin/dev/enro/metadataKeyName.android.kt new file mode 100644 index 00000000..e33ec857 --- /dev/null +++ b/enro-common/src/androidMain/kotlin/dev/enro/metadataKeyName.android.kt @@ -0,0 +1,9 @@ +package dev.enro + +import kotlin.reflect.KClass + +internal actual fun metadataKeyName(kClass: KClass<*>): String { + return kClass.qualifiedName + ?: kClass.simpleName + ?: error("MetadataKey class must have a qualifiedName or simpleName") +} diff --git a/enro-common/src/commonMain/kotlin/dev/enro/NavigationKey.kt b/enro-common/src/commonMain/kotlin/dev/enro/NavigationKey.kt index c2760cf0..0f01b954 100644 --- a/enro-common/src/commonMain/kotlin/dev/enro/NavigationKey.kt +++ b/enro-common/src/commonMain/kotlin/dev/enro/NavigationKey.kt @@ -224,7 +224,7 @@ public interface NavigationKey { public val default: T, ) { public val name: String by lazy { - this::class.qualifiedName ?: error("MetadataKeys must have a valid qualifiedName") + metadataKeyName(this::class) } } diff --git a/enro-common/src/commonMain/kotlin/dev/enro/metadataKeyName.kt b/enro-common/src/commonMain/kotlin/dev/enro/metadataKeyName.kt new file mode 100644 index 00000000..311f7b74 --- /dev/null +++ b/enro-common/src/commonMain/kotlin/dev/enro/metadataKeyName.kt @@ -0,0 +1,21 @@ +package dev.enro + +import kotlin.reflect.KClass + +/** + * Stable string identifier for a [NavigationKey.MetadataKey]'s class. + * Used as the storage key inside [NavigationKey.Metadata] and persisted + * across saved-state round-trips, so the identifier must be: + * + * - **Stable across runs** — serialised metadata must restore to the + * same logical key on next launch. + * - **Unique per MetadataKey class** — two different MetadataKey objects + * must not collide. + * + * On every target except Kotlin/JS this is `KClass.qualifiedName`. On + * Kotlin/JS, `qualifiedName` is unsupported by the Kotlin reflection + * API, so we fall back to `simpleName`. The JS fallback is correct for + * the common `object MyKey : MetadataKey<...>` pattern as long as the + * `MyKey` simple names are unique within the app. + */ +internal expect fun metadataKeyName(kClass: KClass<*>): String diff --git a/enro-common/src/desktopMain/kotlin/dev/enro/metadataKeyName.desktop.kt b/enro-common/src/desktopMain/kotlin/dev/enro/metadataKeyName.desktop.kt new file mode 100644 index 00000000..e33ec857 --- /dev/null +++ b/enro-common/src/desktopMain/kotlin/dev/enro/metadataKeyName.desktop.kt @@ -0,0 +1,9 @@ +package dev.enro + +import kotlin.reflect.KClass + +internal actual fun metadataKeyName(kClass: KClass<*>): String { + return kClass.qualifiedName + ?: kClass.simpleName + ?: error("MetadataKey class must have a qualifiedName or simpleName") +} diff --git a/enro-common/src/iosMain/kotlin/dev/enro/metadataKeyName.ios.kt b/enro-common/src/iosMain/kotlin/dev/enro/metadataKeyName.ios.kt new file mode 100644 index 00000000..e33ec857 --- /dev/null +++ b/enro-common/src/iosMain/kotlin/dev/enro/metadataKeyName.ios.kt @@ -0,0 +1,9 @@ +package dev.enro + +import kotlin.reflect.KClass + +internal actual fun metadataKeyName(kClass: KClass<*>): String { + return kClass.qualifiedName + ?: kClass.simpleName + ?: error("MetadataKey class must have a qualifiedName or simpleName") +} diff --git a/enro-common/src/jsMain/kotlin/dev/enro/metadataKeyName.js.kt b/enro-common/src/jsMain/kotlin/dev/enro/metadataKeyName.js.kt new file mode 100644 index 00000000..95837709 --- /dev/null +++ b/enro-common/src/jsMain/kotlin/dev/enro/metadataKeyName.js.kt @@ -0,0 +1,12 @@ +package dev.enro + +import kotlin.reflect.KClass + +// Kotlin/JS reflection does not expose qualifiedName. simpleName is the +// only stable identifier available — collisions across packages are +// possible in principle, but the recommended `object MyKey : MetadataKey<…>` +// pattern produces a unique simpleName per definition site. +internal actual fun metadataKeyName(kClass: KClass<*>): String { + return kClass.simpleName + ?: error("MetadataKey class must have a simpleName on Kotlin/JS") +} diff --git a/enro-common/src/wasmJsMain/kotlin/dev/enro/metadataKeyName.wasmJs.kt b/enro-common/src/wasmJsMain/kotlin/dev/enro/metadataKeyName.wasmJs.kt new file mode 100644 index 00000000..e33ec857 --- /dev/null +++ b/enro-common/src/wasmJsMain/kotlin/dev/enro/metadataKeyName.wasmJs.kt @@ -0,0 +1,9 @@ +package dev.enro + +import kotlin.reflect.KClass + +internal actual fun metadataKeyName(kClass: KClass<*>): String { + return kClass.qualifiedName + ?: kClass.simpleName + ?: error("MetadataKey class must have a qualifiedName or simpleName") +}