How to use NPM modules in Kotlin/JS - Discord Bot Series (Part 1)
Kevin Schildhorn
Posted on April 12, 2022
KotlinJS is an exciting new method of creating javascript projects that combines the benefits of javascripts speed and versatility and kotlins strong typing and conciseness. Plus the ability to easily add testing and even share code across platforms.
Since KotlinJS transpiles kotlin code to javascript it gives you all the advantages of executing a NodeJS program, while enjoying the benefits of the kotlin language.
There are many use cases for KotlinJS and in this series we'll be creating a Discord bot. Discord is a great platform for keeping in touch with friends, and adding a bot can enrich that experience. We can create this bot by using the NPM module for DiscordJS
.
So how do you create a discord bot using KotlinJS? Well since this is a somewhat large topic this post will be the first in a series of three posts:
- In this post we'll go over how kotlinJS uses node modules. This is more of an intro to KotlinJS and doesn't cover Discord.
- In the next post we'll go over implementing DiscordJS and responding to messages.
- In the last post we'll add some unit tests to make sure the bot works as expected before deploying.
Overview
By the end of this series we'll have a small discord bot that responds to a specific message with the computer name it's running on. In this first part the bot will just print to console on startup, but responding to messages will come later in another part.
Prerequisites
- Some Kotlin Experience
- Some JS Experience
- IntelliJ IDEA (I'm using Community Edition)
New Project
To get started, first follow the js project setup on the kotlin site. (Be sure to choose NodeJS Application
, not browser
)
This will create a project with a Main.kt
file and a greeting function. To run the project, call ./gradlew run
in the terminal. You should see Hello, YOUR_BOT_NAME
in the terminal!
Importing Node Modules in KotlinJS
Lets start off by getting familiar with how KotlinJS handles Node Modules.
To have the bot send a message with its name, let's use a simple node module called computer-name. As mentioned in the project setup, node modules are implemented as dependencies in the build.gradle
file. No need for require
or special imports. Add this line to your dependencies.
dependencies {
implementation(npm("computer-name", "0.1.0"))
}
If you try to call computerName()
now, you'll get an Unresolved reference
error. This is because of a crucial difference between javascript and kotlin:
Javascript is loosely typed and kotlin is strongly typed
So in order to use this function we need to define it by using external functions.
External
The kotlin docs mention the External Modifier, which is used to declare pure javascript code. This tells the compiler that we're expecting this class or function to be defined externally by the node module. Whenever we want to use a module we have to define functions and classes. An important note about this is:
You only need to define what you are using
So you don't need to define the entire module, just what you need to reference.
So for us to use computerName()
we need to define it and tell the compiler where it's defined, like so:
@JsModule("computer-name")
external fun computerName(): String
Note that we also need to annotate the function with the module name or else we'll hit a runtime error(compilerReferenceError
).
This definition was easy to figure out based on the npm page, but as we'll see you may have to dig into the documentation and source code of node modules to find the original definition.
Module System
Another thing we have to define is the module system. You can find more information here, but in short KotlinJS supports UMD
, AMD
and commonJS
systems. commonJS
is widely used for NodeJS so we'll add useCommonJs()
to our build.gradle
file.
kotlin {
js(IR) {
useCommonJs()
}
}
Now try adding println(computerName())
to main()
and run the project.
You should see the name of your computer in the terminal! congrats! Now let's look at importing classes.
Importing NodeJS Classes
So far we've covered importing a function by using computerName()
as an example, but that's a small module with one function. It's also important to be able to import classes and interfaces from modules to use in our project.
For this example we'll use youtube-search, which is a small module that lets you search for youtube videos (Note: You won't get results without a youtube api key, but the calls will still work which is all we need). From the npm page you can see there's a search function that returns custom results. So how do we define this in kotlin?
While there is definition in this module, we'll need to go to the repository to get more information. Lucky for us this module is conveniently written in typescript so it's very easy to find the definition, which is written in index.d.ts
.
Dukat
Before we get into manually converting typescript to kotlin code, I should mention there is a tool to do this automatically called Dukat. It is a powerful tool that can help with easy conversion, however for this post I want to go over how to manually convert typescript to kotlin so that you have a good understanding of how it works.
Manually Converting Typescript
From the repo open the index.d.ts
file and scroll to the bottom to see the search function.
declare function search(
term: string,
opts: search.YouTubeSearchOptions,
cb?: (err: Error, result?: search.YouTubeSearchResults[], pageInfo?: search.YouTubeSearchPageResults) => void
): Promise<{results: search.YouTubeSearchResults[], pageInfo: search.YouTubeSearchPageResults}>;
We can see it takes in:
- a string
- a custom options class
- a callback that returns an optional error, results array, and pageInfo array
The fuction then returns a Promise. Luckily kotlin already defines errors in the stdlib
and it also includes an import for promises:
import kotlin.js.Promise
So the difficult parts are the Results and options. We can see that YouTubeSearchResults
and YouTubeSearchPageResults
are interfaces, so we can easily define them like so:
@file:JsModule("youtube-search")
// Note that for multiple definitions in the same file you can use @file
external interface YouTubeSearchPageResults {
val totalResults: Int
...
}
external interface YouTubeSearchResults {
val id: String
...
}
Now for YouTubeSearchOptions
we could do the same approach, but there's another option we can use.
Json
We can simply use a Json
class object, which acts similarly to a HashMap
. This way we can just pass in what we want without defining the entire interface. This also works well if you can't find a clear definition of an object being passed in.
import kotlin.js.Json
val options = json(
Pair("maxResults", 1),
Pair("key", YOUR_YOUTUBE_API_KEY) // Leave this blank if you don't have one
)
So from all of this, we can then create the external search function:
@JsModule("youtube-search")
external fun search(
term: String,
opts: Json,
cb: (
err: Error?,
result: Array<YouTubeSearchResults>?,
pageInfo: YouTubeSearchPageResults?
) -> Unit // Unit acts as Void in kotlin
) : Promise<Json>
search("your search", options) { err, result, pageInfo ->
print("Youtube callback $err, $result, $pageInfo\n")
}
Try adding this to your project and run. If you don't have an api key you should see this in the terminal:
Youtube callback Error: Request failed with status code 403, null, null
If you do have the api key you should see this:
Youtube callback null, [...], [object Object]
Conclusion
Congratulations you have a working KotlinJS project with imported NPM modules! In the next post we'll go over the specifics of creating our Discord bot using DiscordJS.
Thanks for reading! Let me know in the comments if you have questions. Also, you can reach out to me at @kevinschildhorn on Twitter.
Posted on April 12, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.