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:
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
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,
gradle/libs.versions.toml
is the file that will contain dependency coordinates and version informationlibrary
is the name of our modulelibrary/build.gradle.kts
is where we will add Kotlin/JS build informationMake 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.
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
There are 4 sourceSets here, which each fulfill a different purpose.
commonMain
contains all of the platform-agnostic code. If any platform-specific functionality is needed, it may use the expect
keyword to request it.commonTest
contains the tests for commonMainjsMain
contains the actual
code for anything that is expect
ed by commonMain.jsMain
contains the tests for jsMain.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.
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.
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.