Create a library that compiles to NPM and Jar with Kotlin Multiplatform
A tutorial of building a Kotlin project that compiles to an NPM package (for both NodeJS and browser) and a JVM Jar
But then I remembered: Kotlin can be compiled to either JVM or JS! There must be a way to make it compile the same project to both…
Happily, shortly before that Jetbrains published a new experimental feature in Kotlin — Multiplatform project. Cool!
Immediately I implemented such a library (in Kotlin 1.2), worked really hard to make it work, just to find that two days after I got it working, Kotlin 1.3 was published and completely changed the project structure. That’s the price of using experimental features…
A few months later I found the time to try again. The Kotlin 1.3 approach is indeed much better, and the migration was over in no time. Here is a short tutorial to show you how it is done.
The final code can be found in the GitHub repository.
Before we go into details, one short sad conclusion: Kotlin is not really ready for creating NPM packages. As we’ll see through this tutorial, many things still need to be done manually which makes it really not maintainable. There is also hardly any documentation of how to create an NPM package.
Another important comment: the result really depends on your Kotlin plugin. I wasted few hours trying to figure out (with strange Gradle voodoo) why my resources are not copied into the output Jar or my tests are not running just to find out that updating the plugin resolves the issue.
So make sure you have an updated plugin. I was successful using version 1.3.21 (of plugin and Kotlin jars). Something really strange is going on? Try updating the plugin again, there’s a new version very often.
Starting the project is very simple, as described in the official Kotlin documentation. Just go to
File > New > Project… and select Kotlin (Multiplatform Library):
The result is a ready gradle project with a common directory and a directory for each platform:
common module includes all the code that is common for all platforms. The other modules are code specific for each platform.
You also get a
build.gradle file, which is pretty straight forward. The first thing we may want to do is remove platforms we are not intending to use. In my case, I removed the
mingw modules from the
build.gradle as well as the corresponding folders.
Implementing the common code
Naturally, you would prefer to have maximum code in the
common module, as this will be used for all platforms. However, there are limits: you cannot use any platform-specific library. That means, for example, that you cannot use the Java Standard Library data structures, any gradle/npm dependency that is not based on Kotlin common jar or even use the
Math object, as it is different between Java and JS.
To solve this, Kotlin introduced the
expect keyword. This allows us to declare a class/object/annotation/etc that will be implemented in the platform specific project. For example, in the example file we get when creating a new project, we can see:
This is similar to an interface, and the code now can use this class and object as if they exist. The specific implementation will use the
commonTest we can write tests for common code. It seems like if you have an
expected element in the test, the
actual from the JVM project will be used.
In my case, all the functional code goes into the common module — no need for specific implementation in order to convert units. Hooray!
That also means that all my unit tests are in
commonTest, no need for any platform-specific tests.
Implementing the JVM specifics
jvmMain module we can implement any specific code for JVM or any additional features needed. In my case none was needed. You may notice that there is one class in the
jvmMain module. This was actually added because of JS concerns, we’ll get to that later.
Deploying JVM jars from a multiplatform project
The build process creates a single jar file for JVM in the
/build/libs folder (the one with ‘jvm’ in the name…). This is great for running code, not so much to use in gradle/maven.
Happily, the kotlin-multiplatform plugin supplies us with gradle tasks to help us. In the gradle window, under tasks>publishing there are multiple tasks related to publishing. The most useful ones are
generatePomFileForJvmPublication. I used the latter since I preferred to do the publishing myself. The POM file is created under
/build/publications/jvm and can be used to publish with maven-publish.
One thing you should notice — the jar created does not include any of the KDoc comments from the common code. Sad, but you still get good auto-completion when trying to use in Java, which for my simple case was enough. It is something to consider before using this method for big projects.
Implementing the JS specifics — where things get ugly
jsMain module we can implement specifics for JS. I hoped that also here I wouldn’t need to do anything. That was not quite as smooth as in the JVM case…
The Kotlin2JS architecture is really meant for use as front end main project, not for creating libraries. You’ll find in the documentation guides how to use libraries in your project, but the guides of how to create one don’t really match what happens in reality…
Making an NPM package
First, there is no ready gradle task to create a
package.json file with the information from the
build.gradle. Second, even if there were, the structure of the generated JS is strange. Particularly, the package path becomes objects in the JS. In my case, in Kotlin I used a package name in the JVM convention:
ak.oss.kunitconverter. In JS, if I import the generated module directly, to get to the
UnitConverter class I would need to do:
const unit_converter = require('k-unit-converter');
let converter= //this will be the UnitConverter class
Not so nice for users…
So we need to add to the
resources folder in
jsMain two files:
package.json with the required information (don’t forget to put “kotlin” in the same version like the plugin as a dependency) and
index.js that exposes the content of the package directly (an example below). Also, make sure that the
package.json states the
index.js as the package’s main file.
Another problem that arises was that since Kotlin2JS assumes it’s in a browser, it also assumes that the
kotlin script is included in the page with a
<script> tag, and therefore is in the global scope. This can be tackled with adding to the head of
index.js the following line:
global.kotlin = require(“kotlin”). Very ugly, but currently required.
index.js file looks like this:
Adding definition file
Another problem that arises — just like in the JVM case, the JS is created without any JSDoc. While in the JVM it was bearable for such a simple case, in JS it is a “no-go” — the users of the library won’t even know which parameters they need to supply to the functions!
Since I can’t affect the generated JS, I turned to the other way to describe a JS module — using TypeScript’s
.d.ts description file. I wrote a script that generates the file automatically using reflection on the common code (I don’t include it here because it is too specific). The downside of reflection is that is doesn’t bring the KDoc information, so again we have only the structure with no documentation². Good enough for my purpose, I wouldn’t do it in bigger projects.
The description file (named
index.d.ts) can now be added to the
resources folder in
The main concern about this approach is, of course, that the
index.d.ts is not updated automatically when the common code changes!
The JsName annotation
If we try to use our JS library now, we may find that the names of some of the functions are scrambled (e.g.
hello_61zpoe$). This is an intended behavior of Kotlin to deal with the fact the JS doesn’t have overloads. This is good as long as it is only internal calls, but any public facing API becomes practically unusable for users of the library.
Kotlin gives a solution with the
But wait — this annotation is part of the Kotlin
stdlib-js jar, which means that it can be used only in JS specific code. Not good for our situation where most of the code is common.
This is where the
actual keywords take place. We created an expected annotation
Name, which is fulfilled in the specific platform code:
The JVM implementation exists just because an
expected element must have an
actual implementation in every specific platform. It is not in use in any way.
Publishing as an NPM package
After all this, publishing is pretty straight forward. Unzip the
jar file, and use the
npm publish command. Two things to note, however:
npm versioncommand will not affect our source, so we need to update version number only through the
- We might want to add a
Readme.mdfile to our resources directory, since the file on our root directory is not seen by NPM. This also gives us an opportunity to separate the Readme file for users from the one for developers, which in this case are very different technology wise.
The task of creating a dual-platform unit-conversion library was much more tedious than I expected. While the result is very little code, there was a lot of trial and error on the way.
As I noted throughout the post, Kotlin is not quite ready for the task. However, it definitely is promising and I hope that when the experimental phase of Kotlin Multiplatform is over, it will be a strong tool for package creators that are looking for a way to serve developers in multiple languages.
¹ You may be asking yourself: “why another unit conversion library”? Well, I was very not satisfied with the string-based API that I have seen in all the existing library I have found (at least in JS), and had concerns about the accuracy of some of them since we are dealing with some safety calculate that need to be sufficiently accurate.
² Another approach I tried was using a docklet to create the
.d.ts file. Apparently, KDoc doesn’t have a good equivalent to KDoc yet.