Add Kotlin/JS support to your KMM library
Tags: kotlinjs, kotlin, kotlinmultiplatform, javascript
Published: 2022-03-04 22:23:52

We have been working on a few projects that need to expose Kotlin code through [Kotlin/JS](https://kotlinlang.org/docs/js-overview.html) 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](https://kotlinlang.org/docs/mpp-dsl-reference.html) 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
kotlin {
 sourceSets {
  val commonMain by getting { /* ... */ }
 }
}
```
You may check out [this commit](https://github.com/touchlab/KaMPKit/commit/c7aea3ab33d495c4ec96b32ae9a49193ac8bcd69) 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,

```kotlin
tasks.register<Delete>("clean") {
    delete(rootProject.buildDir)
}
```
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
kotlin {
    // .... other targets
    js(IR) {
        nodejs()
        binaries.library()
    }
}
```
Add `main` and `test` source sets for JS
```kotlin
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"))
        }
    }
}
```
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4kuxnb4j9a5rszb7ognm.gif)

## 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,

```diff
     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
     }
```

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`. 

{% embed https://www.youtube.com/watch?v=ozrEJybnR8g %}

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](https://twitter.com/shaktiman_droid) on Twitter, [LinkedIn](https://www.linkedin.com/in/shaktiman-droid/) or [Kotlin Slack](https://slack.kotlinlang.org/). And if you find all this interesting, maybe you'd like to [work with](https://touchlab.co/contact-us/) or [work at](https://touchlab.co/careers-3/) Touchlab.