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()
}
browser()
It sets the JavaScript target execution environment asbrowser
. It provides a Gradle task—jsBrowserTest
that runs alljs
tests inside the browser usingkarma
andwebpack
.nodejs()
It sets the JavaScript target execution environment asnodejs
. It provides a Gradle task—jsNodeTest
that runs alljs
tests insidenodejs
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
Either
jsBrowserProductionLibraryDistribution
orjsNodeProductionLibraryDistribution
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 thebrowser()
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 bundlejs
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().....
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"
}
}
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"
....
}
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
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!"
}
}
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>
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 thelibrary
referencekmpLib.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;
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
Once installed, the package.json
would contain the library under the dependencies
section,
"dependencies": {
"kmp-lib": "file:../shared/build/productionLibrary"
}
Note here that the
name
is the same as themoduleName
we had defined earlier in the abovegradle-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;
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())
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)
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?