KotlinJS Walkthrough

Posted: 17 Feb 2025. Last modified on 21-Feb-25.

This article will take about 9 minutes to read.



Hosting the JavaScript output of a Kotlin Multiplatform (KMP) project is quite easy. Here’s how you can do it:

Get Acquianted with the Documentation

While Kotlin is a multiplatform language, the current focus is clearly on Compose, and Android/iOS compatibility. Because of that, the documentation for JS is much less present. Even in the Kotlin Multiplatform New Project Wizard, there is no option for Javascript, so we will need to build our own.

JS is unique in that it has 2 different build targets, browser and node. Depending on the project that you are going to be working on, you might choose to start with either one. For my case, I would like to generate a webpacked js file that I can use on my personal site (the one you are probably reading now!)

To that end, we can reference the documentation here

Getting your gradle files in order

First take a look at your gradle files and make sure they look something like this

% tree -P "*toml" -P "*gradle*" --prune
.
├── build.gradle.kts
├── gradle
│   ├── libs.versions.toml
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── library
│   └── build.gradle.kts
└── settings.gradle.kts

4 directories, 9 files

In this case,

Make sure that your libs.versions.toml file contains the proper dependencies.

[versions]
kotlin = "2.0.20"

[libraries]
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }

[plugins]
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }

then update your build.gradle.kts file

plugins {
    alias(libs.plugins.kotlinMultiplatform)
}

group = "io.github.kotlin"
version = "1.0.0"

kotlin {
    js {
        browser {
        }
        binaries.executable()
    }
    sourceSets {
        val commonMain by getting {
            dependencies {
                //put your multiplatform dependencies here
            }
        }
        val commonTest by getting {
            dependencies {
                implementation(libs.kotlin.test)
            }
        }
    }
}

The important part here is that we are importing the kotlin multiplatform plugin, and we are calling

kotlin.js {
    browser {
    }
    binaries.executable()
}

which will make the output browser compatible, including setting up webpack for us.

Create some sample code

The src directory of the project should look pretty familiar if you have worked with Kotlin Multiplatform before.

% tree -P "*kt" --prune
.
└── library
    └── src
        ├── commonMain
        │   └── kotlin
        │       └── CustomFibi.kt
        ├── commonTest
        │   └── kotlin
        │       └── FibiTest.kt
        ├── jsMain
        │   └── kotlin
        │       └── fibiprops.js.kt
        └── jsTest
            └── kotlin
                └── JsFibiTest.kt

11 directories, 4 files

Sourcesets

There are 4 sourceSets here, which each fulfill a different purpose.

Source code

The code in each file is more or less generated from the Kotlin Multiplatform New Project Wizard, but with the missing js source sets manually added.

% for i in $(ag -g '.*\.kt$' | grep -v 'Test'); do echo; echo; echo "//--- $i ---"; cat "$i"; done
//--- library/src/commonMain/kotlin/CustomFibi.kt ---
package io.github.kotlin.fibonacci

import kotlin.js.ExperimentalJsExport
import kotlin.js.JsExport

fun generateFibonacci() = sequence {
    var a = firstElement
    yield(a)
    var b = secondElement
    yield(b)
    while (true) {
        val c = a + b
        yield(c)
        a = b
        b = c
    }
}

@OptIn(ExperimentalJsExport::class)
@JsExport
fun printHello() = println("Hello")

expect val firstElement: Int
expect val secondElement: Int


//--- library/src/jsMain/kotlin/fibiprops.js.kt ---
package io.github.kotlin.fibonacci

actual val firstElement: Int = 2
actual val secondElement: Int = 3

Please note that the output includes 2 files here ^

The template shows an example of expect vs actual. In a normal Kotlin Multiplatform app, we would be assigning the actual values for several other platform sourceSets, not just JS.

It also shows an example of JsExport, which makes JS expose the value by its original name when it is webpacked.

Generating the production webpack file

This part is the easiest, because the gradle is already set up correctly!

Just run ./gradlew jsBrowserProductionWebpack.

There will be no output when it succeeds. However, you can check the result in the build folder.

% tree -A -P '*.js' -I node_modules -I reports -I cache --prune
.
├── build
│   └── js
│       ├── packages
│       │   └── multiplatform-library-template-library
│       │       ├── kotlin
│       │       │   ├── kotlin-kotlin-stdlib.js
│       │       │   ├── kotlin_org_jetbrains_kotlin_kotlin_dom_api_compat.js
│       │       │   └── multiplatform-library-template-library.js
│       │       └── webpack.config.js
│       └── packages_imported
│           └── kotlin-test-js-runner
│               └── 0.0.2
│                   ├── karma-debug-framework.js
│                   ├── karma-debug-runner.js
│                   ├── karma-kotlin-debug-plugin.js
│                   ├── karma-kotlin-reporter.js
│                   ├── karma-webpack-output.js
│                   ├── kotlin-test-karma-runner.js
│                   ├── kotlin-test-nodejs-empty-runner.js
│                   ├── kotlin-test-nodejs-runner.js
│                   ├── mocha-kotlin-reporter.js
│                   ├── tc-log-appender.js
│                   ├── tc-log-error-webpack.js
│                   └── webpack-5-debug.js
└── library
    └── build
        ├── compileSync
        │   └── js
        │       └── main
        │           ├── developmentExecutable
        │           │   └── kotlin
        │           │       ├── kotlin-kotlin-stdlib.js
        │           │       ├── kotlin_org_jetbrains_kotlin_kotlin_dom_api_compat.js
        │           │       └── multiplatform-library-template-library.js
        │           └── productionExecutable
        │               └── kotlin
        │                   ├── kotlin-kotlin-stdlib.js
        │                   ├── kotlin_org_jetbrains_kotlin_kotlin_dom_api_compat.js
        │                   └── multiplatform-library-template-library.js
        ├── dist
        │   └── js
        │       └── productionExecutable
        │           └── library.js
        └── kotlin-webpack
            └── js
                └── productionExecutable
                    └── library.js

24 directories, 24 files

We are specifically looking for the

library/build/kotlin-webpack/js/productionExecutable

file. This is the fully webpacked js file which can be imported directly from an HTML document.

Using the JS file from a Website

First we need to make a folder for the site

# make a new directory for the site
mkdir -p site

# copy the library file into it
cp library/build/kotlin-webpack/js/productionExecutable/library.js site

# go into the site directory
cd site

Then we need to define the index.html file

% cat index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Kotlin/JS + Vanilla JS</title>
  </head>
  <body>
    <h1 id="output">Waiting for Kotlin...</h1>
    <button onclick="callKotlin()">Call Kotlin Function</button>

    <!-- Load Kotlin/JS Output -->
    <script src="library.js"></script>
    <script>
      // Wait until Kotlin module is loaded
      function callKotlin() {
        let myproject = library.io.github.kotlin.fibonacci;
        if (typeof myproject !== "undefined") {
          let result = myproject.getHello();
          document.getElementById("output").innerText = result;
        } else {
          console.error("Kotlin module not loaded.");
        }
      }
    </script>
  </body>
</html>

The most important part here is the library import which creates an object that has the same name as the file

<script src="library.js"></script>

One quirk of importing from kotlin is that it respects package names. To make working with KotlinJS more ergonomic, you might need to create some aliases for the objects that you are working with the most.

This is displayed in the vanilla kotlin function that is called on the button click

let myproject = library.io.github.kotlin.fibonacci;
//...
let result = myproject.getHello();

where we alias the package name object to a JS variable, and call the multiplatform code off of it.

The end result?

On page load

Before image

On button press

After image