Articles / Kotlin Multiplatform: A practical use-case of Kotlin/JS IR compiler

Kotlin Multiplatform: A practical use-case of Kotlin/JS IR compiler

by dzikoysk on 2024-06-15
Kotlin Multiplatform (KMP) tries to move Kotlin out of the JVM world and make it a universal language for all platforms. Well... if something is universal, then it's usually not the best at anything. But let's not be so pessimistic.
A while ago, I was working on a project that required a library that is able to process Hangul (Korean alphabet) input on frontend. Why would you need that? Well, Hangul is a bit more complex than the Latin alphabet, and it's not that easy to process it on the client side. Symbols are combined into syllables, and each syllable is a separate character. Take a look at the following example:
InputInput codeOutputOutput code
0x31470x3147
0x314F0xC544
0x31340xC548
Interesting, isn't it? There are different types of result characters depending on the context (!), for instance:
 + 나어
So now we've got new 2 symbols as a result. You can find more such cases, but in the end it's not a post about Hangul, but about Kotlin Multiplatform. As a requirements for the library, I had the following points:
  1. I need this library on frontend and backend, so I'd like to have a single codebase
  2. The output characters can be calculated using some specific formulas, so I should be able to do some basic operations on code points
  3. I want to have a possibility to easily test the library
Backend was already in Kotlin, so KMP seemed to be a decent choice.

§ Setting up the project

KMP is quite loudly advertised by JetBrains, so I'd expect that setting up the project should be a piece of cake. Well, as usual it is unnecessarily complicated, but could be worse. The Gradle structure looks quite standard:
khangul
├── src
│   ├── commonMain
│   │   └── kotlin
│   │       └── <main sources>
│   ├── commonTest
│   │   └── kotlin
│   │       └── <tests for both jvm and js>
│   ├── jsMain
│   │   └── resources
│   │       └── index.html
├── build.gradle.kts
├── gradle.properties
├── settings.gradle.kts
The setup for the 2 targets I was interested in (JVM and JS) looked like this in Kotlin 1.8:
kotlin {
    js(IR) {
        binaries.library()
        browser()
        generateTypeScriptDefinitions()
    }
 
    jvm {
        jvmToolchain(11)
        withJava()
    }
    sourceSets {
        val commonMain by getting
        val commonTest by getting {
            dependencies {
                implementation(kotlin("test"))
            }
        }
        val jvmMain by getting
        val jvmTest by getting
        val jsMain by getting
        val jsTest by getting
    }
}

§ KMP related issues

§ Kotlin/JS runs on Node with... Yarn

I initially thought they took the simplest way, and the project will just simply run on standard Node.js + NPM setup, so I was quite surprised when I started getting errors from Yarn. Well, it turns out that Gradle cache does not like Yarn lock files, so it's time for our first workaround to actually make it work:
rootProject.plugins.withType(YarnPlugin::class.java) {
    rootProject.the<YarnRootExtension>().yarnLockMismatchReport = YarnLockMismatchReport.WARNING // NONE | FAIL
    rootProject.the<YarnRootExtension>().reportNewYarnLock = false // true
    rootProject.the<YarnRootExtension>().yarnLockAutoReplace = true // true
}

§ Publishing to NPM is not officially supported

As long as you can mark the output as a library with binaries.library(), you don't really have a way to publish it to NPM. In order to do that, you need to use the mpetuska/npm-publish plugin:
npmPublish {
    readme.set(file("README.md"))
    packages {
        named("js") {
            packageJson {
                author {
                    name.set("dzikoysk")
                }
                // ...
            }
        }
    }
    registries {
        register("npmjs") {
            uri.set("https://registry.npmjs.org")
            authToken.set(property("npm.token").toString())
        }
    }
}
The plugin adds some additional tasks, but to get it all in just one command, you still need to register a new one:
tasks.register("publishNpm") {
    dependsOn("clean", "test", "assembleJsPackage", "packJsPackage", "publishJsPackageToNpmjsRegistry")
    tasks.findByName("test")?.mustRunAfter("clean")
    tasks.findByName("kotlinStoreYarnLock")?.dependsOn("kotlinUpgradeYarnLock")
    tasks.findByName("assembleJsPackage")?.mustRunAfter("test")
    tasks.findByName("packJsPackage")?.mustRunAfter("assembleJsPackage")
    tasks.findByName("publishJsPackageToNpmjsRegistry")?.mustRunAfter("packJsPackage")
}
* meh *

§ SDK is not the same as on JVM, obviously

Java has a decent support for code points that would be good enough for me. Unfortunately, it turns out that part of the String class is not ported to Kotlin mappings... This time I was saved by another member of the community, who created a library that provides this missing functionality: cketti/kotlin-codepoints.

§ The library

If you're lucky enough to finish the project, you can publish it to NPM and use it in your frontend project. I actually didn't have any problems with that part, so I can't really complain about it. The bundle size is of course a bit bigger than the one you'd get with a pure JS library, but it's not that bad.
And this is the result! Try to type and see if it really works: