DEV Community

Cover image for Different ways to distribute and integrate Kotlin/JS library
Jigar Brahmbhatt for Touchlab

Posted on

Different ways to distribute and integrate Kotlin/JS library

Note that examples in this post use Kotlin 1.7.0

In the previous post in the Kotlin/JS series, we learned about @JsExport to expose Kotlin code on the JS side. Now, we would look at various ways to distribute a JS library code through a KMP setup.

Table Of Contents

Gradle Setup

If you don't already have a JS target in your KMP library project, then you should check out this blog post first.

Let's look at common options to use for JS target,

js(IR) {
    browser()
    nodejs()
    binaries.library()
    binaries.executable()
}
Enter fullscreen mode Exit fullscreen mode
  • browser()
    It sets the JavaScript target execution environment as browser. It provides a Gradle task—jsBrowserTest that runs all js tests inside the browser using karma and webpack.

  • nodejs()
    It sets the JavaScript target execution environment as nodejs. It provides a Gradle task—jsNodeTest that runs all js tests inside nodejs using the built-in test framework.

  • binaries.library()
    It tells the Kotlin compiler to produce Kotlin/JS code as a distributable node library. Depending on which target you've used along with this, you would get Gradle tasks to generate library distribution files.

    Kotlin browser tasks
    --------------------
    jsBrowserDevelopmentLibraryDistribution
    jsBrowserDevelopmentLibraryPrepare
    jsBrowserProductionLibraryDistribution
    jsBrowserProductionLibraryPrepare
    
    Kotlin node tasks
    -----------------
    jsNodeDevelopmentLibraryDistribution
    jsNodeDevelopmentLibraryPrepare
    jsNodeDevelopmentRun
    jsNodeProductionLibraryDistribution
    jsNodeProductionLibraryPrepare
    jsNodeProductionRun
    jsNodeRun
    

    EitherjsBrowserProductionLibraryDistribution or jsNodeProductionLibraryDistribution task generates output files in <YourLibModule>/build/productionLibrary folder

  • binaries.executable()
    It tells the Kotlin compiler to produce Kotlin/JS code as webpack executable .js files. Enabling this option generates the following Gradle tasks with the browser() environment.

    jsBrowserDevelopmentExecutableDistributeResources
    jsBrowserDevelopmentExecutableDistribution
    jsBrowserDevelopmentRun - start development webpack dev server
    jsBrowserDevelopmentWebpack - build webpack development bundle
    jsBrowserDistribution
    jsBrowserProductionExecutableDistributeResources
    jsBrowserProductionRun - start production webpack dev server
    jsBrowserProductionWebpack - build webpack production bundle
    jsBrowserRun
    jsBrowserWebpack
    

    Task jsBrowserDistribution generates webpack bundle js file along with .map file in <YourLibModule>/build/distributions folder.

Webpack

Webpack is a JS bundler tool. It allows the creation of a single executable JS file combining all the dependencies.

As seen above in the Gradle setup section, you can output your Kotlin/JS library as a single executable by using browser() and binaries.executable() options in js(IR) target. By default, the bundle .js file and exported library name are the same as the module name. For example, for a module named shared, a shared.js file gets generated. It would have exports.shared=... that reflects the name of the library that one would use when consuming this library in a JS/TS app.

....."object"==typeof exports?exports.shared=n().....
Enter fullscreen mode Exit fullscreen mode

The browser() function provides a DSL that you can use to update KotlinWebpack task for various custom configurations. For example, you can change the file and library name as below,

browser {
    webpackTask {
        outputFileName = "kmp_lib.js"
        output.library = "kmpLib"
    }
}
Enter fullscreen mode Exit fullscreen mode

You can explore more options on your own.

Node Module

Kotlin/JS library can also be published as a node module, and installed via npm in an app.

As mentioned above in the Gradle setup section, when you run jsNodeProductionLibraryDistribution, it generates the following node-module related files,
- .js file each for the library and all dependencies
- .js.map file for each .js file
- Typescript .d.ts file
- package.json

Note that node module cannot have upper case character in the name. An error or warning may appear if the module name has a capital character. We can declare a moduleName in the js(IR) target to avoid that,

js(IR) {
    moduleName = "kmp-lib"
    ....
}
Enter fullscreen mode Exit fullscreen mode

npm-publish plugin

By default, there is no quick way to publish the library output as a node module to maven.

npm-publish is a popular library by Martynas Petuška that helps with NPM publishing. It also provides various configuration options under the npmPublish Gradle task.

The plugin generates a tarball that we can use to install the library locally without publishing first. The packJsPackage task also logs a readable output of tarball content and details.

npm notice 
npm notice 📦  shared@0.1.0
npm notice === Tarball Contents === 
npm notice 291B  kmp-lib.d.ts                     
npm notice 1.4kB kmp-lib.js                       
npm notice 205B  kmp-lib.js.map                   
npm notice 1.4kB kotlin-kotlin-stdlib-js-ir.js    
npm notice 551B  kotlin-kotlin-stdlib-js-ir.js.map
npm notice 95B   package.json                     
npm notice === Tarball Details === 
npm notice name:          shared                                  
npm notice version:       0.1.0                                   
npm notice filename:      shared-0.1.0.tgz                        
npm notice package size:  1.6 kB                                  
npm notice unpacked size: 3.9 kB                                  
shared-0.1.0.tgz
npm notice shasum:        c3c5e0a7a6d99cf1b7632fcfc4cef2e0b9052cb2
npm notice integrity:     sha512-dZa4uNPRMjyny[...]JjzzNAiP7abHw==
npm notice total files:   6                                       
npm notice 
Enter fullscreen mode Exit fullscreen mode

If you're planning to publish a Kotlin/JS library as a node module, then this is a must-have plugin in your project.


Using the library

We will now look at various ways to integrate the Kotlin/JS library into web applications.

For example, we have a Greeting class and a greet() method under a package jabbar.jigariyo.kmplibrary

package jabbar.jigariyo.kmplibrary

import kotlin.js.JsExport

@JsExport
class Greeting {
    fun greet(): String {
        return "Hello, JS!"
    }
}
Enter fullscreen mode Exit fullscreen mode

Use webpack executable

We will now look at using the generated webpack binary executable bundle file in HTML and react JS app.

HTML

We can simply embed the js file as a script under the <script> tag of the HTML body.

<html>

<head>
    <meta charset="utf-8">
    <title>Including a External JavaScript File</title>
</head>

<body>
    <!-- Here I have copied the js file in the root folder of this html file -->
    <script type="text/javascript" src="kmp_lib.js"></script>
    <script>
        var kmpLib = this['kmpLib'];
        var greeting = new kmpLib.jabbar.jigariyo.kmplibrary.Greeting();
        console.log(greeting.greet());
    </script>
</body>

</html> 
Enter fullscreen mode Exit fullscreen mode

Two important things to note here are,

  • Using the library name to get a reference var kmpLib = this['kmpLib']
  • Getting reference of Greeting class using package name after the library reference kmpLib.jabbar.jigariyo.kmplibrary.Greeting()

Once the html file is open in the browser, it would log Hello, JS! in console output.

JavaScript

For the JavaScript example, I've a React app.

To keep this simple, I've copied the distributions folder in the src directory of the React app.

As the next step, import the library module in the App.js and use it

import kmpLib from './distributions/kmp_lib';
......

const greeting = new kmpLib.jabbar.jigariyo.kmplibrary.Greeting()
console.log(greeting.greet())

function App() {
  return (
    .......
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Once we run the app, Hello, JS! gets printed in the console and validates the integration with the library.

Note that you might have to deal with configuring eslint if using React.

TypeScript

It's not straightforward to use just a js file without typescript definitions in a Typescript app.

I have skipped this integration as it's out of scope for this post.

Use as node module

We will now look at installing and using the generated library files in a React JS and a TS app.

To install the module locally, you can do npm install <PATH>. You can either use the path to the tarball (.tgz) file generated by the npm-publish plugin or the relative path to the build/productionLibrary folder of the kmp module.

For example, the installation path for my sample looks like this,

npm install ../shared/build/productionLibrary
Enter fullscreen mode Exit fullscreen mode

Once installed, the package.json would contain the library under the dependencies section,

"dependencies": {
  "kmp-lib": "file:../shared/build/productionLibrary"
}
Enter fullscreen mode Exit fullscreen mode

Note here that the name is the same as the moduleName we had defined earlier in the above gradle-setup section.

HTML

Using node module in plain HTML is out of scope for this post. There are techniques to make that work if you need to go that route.

JavaScript

We should be able to import the library and use it once installed via npm

App.js
import kmpLib from 'kmp-lib';
......

const greeting = new kmpLib.jabbar.jigariyo.kmplibrary.Greeting()
console.log(greeting.greet())

function App() {
  return (
     .....
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Once we run the app, Hello, JS! gets printed in the console and validates the integration with the library.

TypeScript

Just like the JavaScript example above, you have a reference of the library available after npm install.

Importing in Typescript is slightly different as seen below

main.ts
import * as kmpLib from 'kmp-lib';

let greeting = new kmpLib.jabbar.jigariyo.kmplibrary.Greeting()
console.log(greeting.greet())
Enter fullscreen mode Exit fullscreen mode

Once we run tsc main.ts and then node main.js commands in the terminal, we would see Hello, JS! printed.


In this post, we first learned how to distribute a Kotlin/JS library as either a webpack bundle or node module. We later learned how to integrate the library in HTML, JS, or a TS app.


Thanks for reading! Let me know in the comments if you have questions. Also, you can reach out to me at @shaktiman_droid on Twitter, LinkedIn or Kotlin Slack. And if you find all this interesting, maybe you'd like to work with or work at Touchlab.

Top comments (1)

Collapse
 
nbransby profile image
nbransby

Great guide - can find very little about this process in the official documentation, how did you learn about it?

I followed the steps for Use as node module - TypeScript but when I run my app in the browser I get "Uncaught TypeError: Cannot read properties of undefined (reading 'kotlin-kotlin-stdlib-js-ir')"

I am using Vite if that might be a factor?