Create a library that compiles to NPM and Jar with Kotlin Multiplatform

Alon Kashtan
The Startup
Published in
8 min readJul 4, 2019

A tutorial of building a Kotlin project that compiles to an NPM package (for both NodeJS and browser) and a JVM Jar

Recently I started to write a simple library for unit conversion¹ for our organization. This brought up an interesting dilemma: we are using both Java and Javascript, and it is important that the conversion will be the same in all places. Writing two libraries and making sure they are functioning the same seemed not very maintainable even for such a small project.
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.

Setting up

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:

The 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 actual keyword.

in 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

In the 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 publishJvmPublicationToMavenLocal and 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

In the 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
unit_converter["k-unit-converter"].ak.oss.kunitconverter

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.

So the 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 jsMain.

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() becomes 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 @JsName annotation, which gets as a string the name of the function that is generated in Javascript. We need to put this annotation over every method (at least where we use overloads).

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 expect and 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:

  1. The npm version command will not affect our source, so we need to update version number only through the build.gradle.
  2. We might want to add a Readme.md file 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.

Conclusion

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.

______________
Footnotes:
¹ 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.

--

--

Alon Kashtan
The Startup

I’m an experianced software architect and developer that is excited about code and sharing knowledge. My Hebrew blog: https://blog.alon-k.com/