DEV Community

Cover image for Add Kotlin/JS support to your KMM library
Jigar Brahmbhatt for Touchlab

Posted on

Add Kotlin/JS support to your KMM library

We have been working on a few projects that need to expose Kotlin code through Kotlin/JS as an external JS library. You can add JS as an output target of an existing KMM-focused module, but there are some issues you'll need to consider that don't generally present challenges to a mobile-only project.

Note that this post assumes that you already have a Kotlin Multiplatform Mobile library project, and are planning to add Kotlin/JS support to it

A good f̵i̵r̵s̵t̵ zero-step (not a mandatory one) would be to make sure that your source sets are marked by getting as per the Kotlin Gradle DSL standards. It only applies if you use Kotlin based build scripts.

I would strongly recommend moving to Kotlin based scripts If you're still using Groovy-based Gradle build scripts

This Multiplatform Gradle DSL reference is a helpful document to follow while writing a Gradle build script for KMP.

After this step, your build script would have source sets declared as below,

kotlin {
 sourceSets {
  val commonMain by getting { /* ... */ }
 }
}
Enter fullscreen mode Exit fullscreen mode

You may check out this commit where I made these changes for the KaMPKit project

Now let's move to actual steps


Step 1

Make sure that you remove any clean task from your project. Gradle's LifecycleBasePlugin already brings the clean task, so you want to avoid getting a compilation error later. You most likely have one in your root Gradle file that looks like this,

tasks.register<Delete>("clean") {
    delete(rootProject.buildDir)
}
Enter fullscreen mode Exit fullscreen mode

Add the JS target block with IR compiler option to your kotlin block, and add the nodejs target and library container inside that

We will discuss both options in detail later

kotlin {
    // .... other targets
    js(IR) {
        nodejs()
        binaries.library()
    }
}
Enter fullscreen mode Exit fullscreen mode

Add main and test source sets for JS

sourceSets {
    // .... other source sets
    val jsMain by getting
    val jsTest by getting {
        dependencies {
            // you don't need this if you already have
            // kotlin("test") as your `commonTest` dependency
            implementation(kotlin("test-js"))
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

If you sync the project now, it should sync successfully! (probably won't build yet)

Now add the actual JS source folders.

Since we've already added JS target, we can add the jsMain and jsTest directories using auto complete by right-clicking on src --> new --> Directory

Animation to show how to add JavaScript source set

Step 2

At this stage, your project might not compile if you have any code in commonMain that Kotlin/JS does not support, or if it is missing JS equivalents. ./gradlew build would most likely fail.

You now have two options,

1) Make sure all your common code compiles for JS, can be exported as JS library and add js actual for any expect declarations

2) Introduce a mobileMain source set/folder and move all the existing common code there

I would suggest going with option (2) because it is a path of the least resistance, and you would get more time to think about how you want to write JS-specific code and common code for all platforms later. You may already have a lot of existing code in commonMain with various dependencies that might not be suitable to use on JS.

Native mobile platforms, Android and iOS tend to have similar needs and capabilities like SQL, local files, threads, serialization, etc. JS/web, on the other hand, are somewhat different in what you can do and often work differently. It makes sense then that any moderately functional library will need at least consideration of the conceptual differences and quite probably another layer (mobileMain) to better separate features and dependencies between web and native mobile. The latter is generally what we recommend, because you’ll probably need to do that separation at some point anyway.

option (1) is the fallback if you have a small existing codebase that requires only a few changes to support the JS side, and if you would most likely not have any common code between android and iOS platforms.

For option 2

First, you would need to create custom source sets for mobileMain and mobileTest and make android and ios ones depend on it. Also, note that you will need to move all dependencies from commonMain to mobileMain. In short, you would want mobileMain to look like your commonMain after the change. commonMain would get emptied.

Before and after diff of mentioned changes look like this for a sample project,

     sourceSets {
-        val commonMain by getting {
-            dependencies {
-                implementation("io.ktor:ktor-client-core:$ktorVersion")
-            }
-        }
+        val commonMain by getting
         val commonTest by getting {
             dependencies {
                 implementation(kotlin("test"))
             }
         }
         val iosArm64Main by getting
         val iosSimulatorArm64Main by getting
         val iosMain by creating {
-            dependsOn(commonMain)
             iosX64Main.dependsOn(this)
             iosArm64Main.dependsOn(this)
             iosSimulatorArm64Main.dependsOn(this)
         }
         val iosArm64Test by getting
         val iosSimulatorArm64Test by getting
         val iosTest by creating {
-            dependsOn(commonTest)
             iosX64Test.dependsOn(this)
             iosArm64Test.dependsOn(this)
             iosSimulatorArm64Test.dependsOn(this)
         }
+        val mobileMain by creating {
+            dependsOn(commonMain)
+            androidMain.dependsOn(this)
+            iosMain.dependsOn(this)
+            dependencies {
+                implementation("io.ktor:ktor-client-core:$ktorVersion")
+            }
+        }
+        val mobileTest by creating {
+            dependsOn(commonTest)
+            androidTest.dependsOn(this)
+            iosTest.dependsOn(this)
+        }
         val jsMain by getting
         val jsTest by getting
     }
Enter fullscreen mode Exit fullscreen mode

Next, you would add the actual folder, just like what we did for js above in step 1. Along with that, you would want to move the entire commonMain code content to mobileMain, and commonTest to mobileTest.

After this, your project should build successfully with ./gradlew build because you would not have any code in commonMain that failed before due to introduction of JS side.

Step 3

Now you're ready to work on the JS codebase.

You might end up moving some classes back from mobileMain to commonMain depending on what you want in all three platforms, but at this point you can build and test the project after every step so that you're sure nothing is breaking.


Now that you have the JS sourceSet, in the next post we will look at writing exportable code in Kotlin for JS using @JsExport


Thanks for reading! Let me know in the comments if you have questions. Also, you can reach out to me at @shaktiman_droid on Twitter, LinkedIn or Kotlin Slack. And if you find all this interesting, maybe you'd like to work with or work at Touchlab.

Discussion (0)