Hi :wave: my team has been writing shared business...
# multiplatform
j
Hi đź‘‹ my team has been writing shared business logic in KMM for our native apps and web apps. When our kotlin data classes is exported to JS the
val
names are replaced. For the given class,
Copy code
@JsExport
data class Foo (
  val configs: Configs?,
)
I would expect the exported JS to have a key
configs
but instead it is replaced with
this.b1q_1 = configs;
Is there any way to preserve the human readable val names within our exported class?
e
Copy code
@JsName("configs")
val configs: Configs?
j
Even with the @JsName annotation, the built js export seems unchanged. Are there any changes that I would need to make in the build.gradle file?
My
js
config for KotlinMultiplatformExtension is
Copy code
js(IR) {
        browser {
            testTask {
                useMocha {
                    timeout = "10s"
                }
            }
        }
        binaries.library()
        browser()
    }
and I am using mpetuska’s
npm-publish
lib to assemble the js package.
b
I don't think I ever heard anyone successfully exporting a data class, but I suspect that you might need to look for something like getConfigs() since you're exporting properties and not backing fields. To work around it, you could export an interface and a factory function that would simply return a data class implementing the interface. Either that or switch to a regular class
j
@Big Chungus Thanks for the suggestions 🙏. I tried switching usage of a
data class
to a regular
class
but it seems like the variable names are still not surfaced. Here are some screenshots of
.kt
->
.js
->
.d.ts
b
Hmm, smells like a bug then since your d.ts is correct
j
Do you mean a bug in the implementation of the class? Or a bug in assembling the JS package? Or is it unclear?
b
A bug on kotlin compiler side
m
So the kotlin/js compiler obfuscates all of the properties of classes (i think to allow custom getters an read only/val properties) and generates wrapper functions to access them. so the
configs
property is hidden as
b1q_1
in your case to prevent anyone from using the field directly. to give access the compiler generates a statement like this which should allow you to access the property as
foo.configs
that defined property on the Foo prototype is what the d.ts file points to with the
get configs(): Nullable<…Configs>
For
var
you’ll have a similar defineProperty call that also provides a set, as well as a corresponding
set configs(): ...
line in the
d.ts
e
https://pl.kotl.in/L_Rhy0jms looks like the current compiler already generates
Object.defineProperty(Foo.prototype, 'configs', { ... })
j
@Michael Friend Thanks for the explanation 🙇. Accessing the vars is not really the issue. It is more so that we have to parse this class by extracting the props before storing to our state manager (Redux in this case). I am looking for a way to skip the parsing step.
j
Inspecting the state is an essential part of developing with Redux. Having obfuscated vars makes that impossible.
Can we make it an option? It should be up to the developer to decide whether it should be obfuscated
m
Ah i see, needing to rely on the property name itself definitely complicates things. As far as i know theres not a way to have the kotlin compiler not obfuscate since it would break a few features of kotlin like custom get/set and read only
val
properties. you may be stuck just writing a wrapper class in js around your Kotlin classes so you can use them in redux
j
How about this? Instead of having obfuscated property names, use a more readable private property name? For example, if the property name is “foo”, use “__foo” instead of “x2a_1”._
The whole issue is to have human-readable state in Redux. Even “foo_x2a_1” would help. The code still access the property through foo, but this will help to inspect the Redux state
m
that is actually done in some cases. For the project i was testing in it actually uses
foo_1
but im not sure what causes it to be more readable. Probably changes in different kotlin versions, this is on 1.6.21
j
I am on 1.6.21
m
hm thats strange. not sure why it would be different
Maybe a prod vs debug development build? thats my only other guess
j
There are prod vs debug flavor of IR compiler?
m
Im actually not sure tbh. Ive got a
development
and
production
flavors of the JS gradle tasks but they both produce
configs_1
for me so im not sure why yours is different
message has been deleted
j
It would be nice to have a public repo of a starter project which we can all look at
I don’t have anything like this in my project lol
m
im just messing around in a personal project of mine right now. Its public so if you want to just pull it down and try it locally feel free. Heres the branch and i have the js testing stuff in the
collection-import
module. I can take some time to try to throw together a standalone repo with just js test stuff sometime tonight/tomorrow if you want something simpler https://github.com/mrf7/KMMtg/tree/test/js-testing
j
You rock! I will try it
The only difference in build.gradle is
binaries.executable()
vs
binaries.library()
I think the cause is
@JsExport
, which we have to use for
binaries.library()
or just
binaries.library()
, even without
@JsExport
, the members are still obfuscated
m
oh sorry didnt realized i had removed
@JsExport
by mistake. I readded it and switched from executable to library and im still getting
configs_1
🤷 pushed to that branch if you want to try it
j
sorry, a quick question. Which gradle task do I use to generate the js?
gradle build
fails. I have tried
gradle jsPackageJson
and
gradle jsMainClasses
and I cannot find the generated js files.
m
I'm afk right now so I'm not sure what it was called exactly but it was in a group called
kotlinBrowser
I think. Pretty sure it was something like
somethingProductionWebpack
b
Should be jsBrowserDistribution. Then look for your deployable bundle in build/distribution/js
m
Yea that sounds right
b
Or just for js jsBrowserSync
Or was it jsBrowserProductionWebpack maybe
m
Oh yea I think that was it actually
j
And I copy the same code into my project and got
Copy code
function Foo(configs) {
  this.d26_1 = configs;
}
m
That's very strange. And no difference in the Gradle setup?
j
Your project generates friendly member names with both jsBrowserDevelopmentLibraryDistribution and jsBrowserProductionLibraryDistribution
My project generates friendly names with jsBrowserDevelopmentLibraryDistribution but not jsBrowserProductionLibraryDistribution
I had the same js section in build.gradle.kt
m
hmm thats quite odd. Are you able to share the rest of the build.gradle?
j
Copy code
import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework

buildscript {
    val agp_version by extra("7.2.2")
    repositories {
        //gradlePluginPortal()
        //google()
        mavenCentral()
    }
    dependencies {
        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.0")
        classpath("com.android.tools.build:gradle:$agp_version")
    }
}

allprojects {
    repositories {
        google()
        mavenCentral()
    }
}

plugins {
    kotlin("multiplatform") version "1.6.21"
    kotlin("plugin.serialization") version "1.4.0"
    id("maven-publish")
}

group = "exchange.dydx.abacus"
version = "1.0.27"

repositories {
    google()
    mavenCentral()
}

kotlin {
    js(IR) {
        browser {
            testTask {
                useMocha {
                    timeout = "10s"
                }
            }
        }
        browser()
        binaries.library()
    }

    sourceSets {
        val ktorVersion = "2.1.1"
        val napierVersion = "2.6.1"
        all {
            languageSettings.apply {
                optIn("kotlin.js.ExperimentalJsExport")
            }
        }

        val commonMain by getting {
            dependencies {
                implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
                implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.0-RC")
                implementation("io.github.aakira:napier:$napierVersion")
                implementation("co.touchlab:stately-common:1.2.0")
                implementation("io.ktor:ktor-client-core:$ktorVersion")
                implementation("com.ionspin.kotlin:bignum:0.3.7")
            }
        }
        val commonTest by getting {
            dependencies {
                implementation(kotlin("test"))
                implementation("org.jetbrains.kotlin:kotlin-test-common")
                implementation("org.jetbrains.kotlin:kotlin-test-annotations-common")
            }
        }
        val jsMain by getting
        val jsTest by getting
    }

    tasks.wrapper {
        gradleVersion = "7.5.1"
        distributionType = Wrapper.DistributionType.ALL
    }
}
Trimmed down to have Js build only, and still able to reproduce
m
Alright i think ive figured out a path forward for you. Did some fiddling around with kotlin version and LEGACY vs IR compiler and the results look like this
Copy code
1.6.21 IR gives str_1

1.6.21 LEGACY gives str (unobfuscated)

1.7.0 IR gives c_0

1.7.21 IR gives c_0

1.7.21 LEGACY gives str (unobfuscated)

1.8.0 IR gives str (unobfuscated)
have a couple of the above configurations on appropriately named branches in this test repo https://github.com/mrf7/js-playground
I noticed that this doesnt match up with what youre seeing given your config (1.6.21, IR compiler) and noticed i hadnt added the buildscript block to get the kotlin gradle plugin. Not sure what situations that bit is or isnt necessary cuz im not a gradle guru, but the important part is that when i added exactly what you have i got the
c_0
obfuscated names you were seeing on 1.6.21 with the IR compiler.
And then something caught my eye. You’re using kotlin multiplatform 1.6.21, but gradle plugin 1.7.0. Im not sure exactly what this means in terms of compilation etc, but what i did figure out is that changing that to 1.6.21 instead to match the kotlin version youre using gives you
str_1
i.e. obfuscated but still human readable property name. And that is the one difference between what you had and the other project i posted. But in that project it was multimodule so that declaration was in a different file (root build.gradle)
Copy code
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.0")
so im guessing that in the 1.7.0 release they made the switch (in the ir complier only) to fully obfuscate the property names. And for whatever reason using the 1.7.0 gradle plugin pulls that change in. Bumping that down should give you the behavior you were looking for but thats not a great long term solution since it pins you to 1.6.21 so probably worth filing a bug/request and calling out Redux as a use case that requires human readable names
Good news! the recent 1.8.0-Beta kotlin release goes back to a completely unobfuscated property name so you can just skip 1.7 and go to 1.8 when you need to upgrade
j
Thanks! I tried different versions. Unfortunately, my project built with different errors if I set org.jetbrains.kotlin:kotlin-gradle-plugin to 1.6.21 or 1.8.0-Beta. LEGACY is not compatible with binaries.library(), so we are stuck on 1.7.21 IR
I hope 1.8 final version will work better
m
What were the errors you got on the other versions
j
One issue I am getting with 1.8.0-Beta (either with gradle-plugin or multiplatform) is this error
Copy code
Execution failed for task ':kotlinStoreYarnLock'.
> yarn.lock was changed. Run the `kotlinUpgradeYarnLock` task to actualize yarn.lock file
m
Oh yea I got that too. Running that Gradle task worked for me
j
Thanks! I didn’t realize it is a gradle task
beautiful, that fixed it
m
Yea that error could be a bit more clear for sure
j
And 1.8.0-Beta indeed fixed the naming problem!
m
Awesome! Keep in mind that properties with custom getters or setters will still be obfuscated and not human readable, and so will anything in a class that doesn't have JsExport on it. This branch shows off some different scenarios with the Js code that gets generated https://github.com/mrf7/js-playground/blob/180-showcase/src/commonMain/kotlin/Foo.kt
j
👍