Skip to content

JavaScript (Kotlin/JS)

Kargo compiles Kotlin to JavaScript with the Kotlin/JS compiler (the IR backend, js(IR) in Gradle). Use it when the code has to run where WebAssembly doesn’t: older browsers, Node.js tools, npm packages that plain JavaScript projects consume, and multiplatform libraries whose users declare js(). No Gradle and no webpack are involved; Kargo only runs npm, pnpm or bun when the project needs npm packages.

For new browser apps, WebAssembly is usually faster. The two targets are separate: a project can enable both, with src-wasm/ and src-js/ for their own code.

kargo.toml
[target.js]
environment = "node" # "node" (default) or "browser"
Environment Use it for Libraries Kargo adds
node (default) Programs and libraries that run on Node.js. kargo run and kargo test run them with node. kotlin-stdlib-js; kotlin-test-js for tests
browser Web pages. Kargo adds an HTML page, serves it with kargo run and runs the tests in a headless browser. The same, and kotlinx-browser (document, window, org.w3c.dom)

kargo new hello -t js creates a project with this target, and kargo target add js adds it to an existing one. See kargo target.

You need Node.js on your PATH for kargo run -t js and kargo test -t js, but not for building.

Terminal window
$ kargo new hello -t js
$ cd hello
$ kargo run -- a b
Compiling hello v0.1.0 (js)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 5.50s
Running `node target/debug/js/hello.mjs a b`
Hello, Kotlin!

Arguments after -- reach main(args), as on the JVM: Kargo has the compiler pass process.argv after the script path to it. Build output goes to target/<profile>/js/.

Terminal window
$ kargo test
Compiling hello v0.1.0 (js, tests)
Running Kotlin/JS tests (Node.js)
test com.example.hello.GreetingTest.greetsByName ... ok (0ms)
test result: ok. 1 passed; 0 failed; 0 ignored; finished in 510ms

Tests use kotlin.test as on every other target. Kargo runs them with a small runner of its own, so there is no Mocha or Karma to install: the runner collects the tests kotlin-test registers when the test module loads, runs them one after the other (waiting for the ones that return a Promise, such as runTest from kotlinx-coroutines-test) and reports them to Kargo. What the tests print, failures with their stack traces, @Ignore, a test name filter (kargo test GreetingTest), --report junit and --message-format json all work. Dev builds have source maps and Kargo starts Node.js with --enable-source-maps, so stack traces point at your Kotlin files.

kargo.toml
[target.js]
environment = "browser"
src/Main.kt
package com.example.hello
import kotlinx.browser.document
fun main() {
document.body?.append("Hello from Kotlin/JS")
}
Terminal window
$ kargo run -t js
Compiling hello v0.1.0 (js)
Serving target/debug/js at http://127.0.0.1:8080/ (press Ctrl+C to stop)

kargo run serves the app the way it serves a Wasm browser app, with the same --port and --open options. Kargo writes index.html next to the module, unless resources/index.html is your own page; everything below resources/ is copied to target/<profile>/js/. The generated page loads <name>.mjs with <script type="module">, or <name>.js with a plain <script> for a UMD module.

kargo test runs the tests in a headless browser, found and configured as for Wasm (KARGO_BROWSER, KARGO_BROWSER_ARGS, KARGO_BROWSER_TIMEOUT). Any current browser runs Kotlin/JS.

The compiler writes an ES module by default. Set module-kind for other consumers:

kargo.toml
[target.js]
module-kind = "commonjs" # "es" (default), "commonjs" or "umd"
module-kind Files Loaded with
es (default) <name>.mjs import, <script type="module">
commonjs <name>.js require on Node.js
umd <name>.js require, AMD, or a plain <script> tag, which puts the module on globalThis

A browser can’t load a CommonJS module, so a browser application needs es or umd. Tests always run as an ES module, whatever the program is. module-name changes <name> (default: the package name).

kargo build --release links with the compiler’s dead code elimination (-Xir-dce): only what main and @JsExport reach stays, and no source map is written. For a library with one exported function that takes the module from about 560 KiB to under 1 KiB. Dev builds keep everything and write <name>.mjs.map with the Kotlin sources embedded.

Set kind = "library" and mark what JavaScript and TypeScript may use with @JsExport:

kargo.toml
[package]
name = "greeter"
version = "0.3.0"
license = "MIT"
[target.js]
kind = "library"
npm-name = "@acme/greeter" # default: the lowercase package name
src/Greeter.kt
package com.example.greeter
@OptIn(ExperimentalJsExport::class)
@JsExport
fun greet(name: String): String = "Hello, $name!"
Terminal window
$ kargo build --release
Compiling greeter v0.3.0 (js)
$ ls target/release/js
greeter.d.mts greeter.mjs package.json

target/<profile>/js/ is an npm package: publish it with npm publish target/release/js.

  • Declarations. <name>.d.mts (<name>.d.ts for CommonJS and UMD) holds the TypeScript declarations of the @JsExport declarations (export declare function greet(name: string): string;). Unlike Wasm, Kotlin/JS exports classes and objects too. An ES module exports them by name (import { greet } from "@acme/greeter"); CommonJS and UMD modules put them in objects named after their Kotlin package (require("@acme/greeter").com.example.greeter.greet).
  • package.json comes from [package] (name, version, description, license, author), with type (module, or commonjs for the other kinds), main, types, exports, files, and the npm packages the library needs as dependencies.
  • A library has no main, so kargo run refuses to run it; kargo test runs its tests.

To publish a Kotlin library for other Kotlin projects, use kargo publish: it publishes the klib as the <name>-js module, with the Gradle module metadata (org.jetbrains.kotlin.platform.type = js, org.jetbrains.kotlin.js.compiler = ir) that Gradle and Kargo builds select it by.

Kotlin/JS libraries that import npm packages at runtime list them in a package.json inside their klib. Kargo installs them after compiling, with the package manager rules described for Wasm: the first of npm, pnpm and bun on your PATH (or package-manager), lifecycle scripts off (--ignore-scripts), the highest version when two libraries disagree, and --offline passed on.

The packages go to target/<profile>/js/node_modules, next to the module, where Node.js finds them. For a browser app, the page gets an import map for them and kargo run serves them at /node_modules/ (an ES module app only; a UMD page can’t use an import map).

For an npm package of your own, list it in [target.js.npm-dependencies] and declare what you use with @JsModule:

kargo.toml
[target.js.npm-dependencies]
left-pad = "1.3.0"
src/Main.kt
@JsModule("left-pad")
external fun leftPad(text: String, length: Int, ch: String): String
fun main() = println("[" + leftPad("7", 3, "0") + "]")
Terminal window
$ kargo run
Installing npm packages left-pad@1.3.0 (npm)
Compiling user v0.1.0 (js)
Running `node target/debug/js/user.mjs`
[007]

Unlike on Wasm, Kargo doesn’t generate Kotlin declarations from an npm package’s TypeScript declarations for Kotlin/JS: write the external declarations you need, or use dynamic. The Kotlin docs cover JavaScript interop and @JsModule.

src/ is common code for every enabled target and src-js/ the JS target’s own, with tests-js/ for its tests. expect declarations in src/ get their actual in src-js/:

kargo.toml
[target.jvm]
[target.js]
src-js/Platform.kt
package com.example.mpp
actual fun platform() = "js"

Dependencies only the JS target uses go in [target.js.dependencies]. Kargo resolves every dependency to its Kotlin/JS variant from Gradle module metadata, the same way Gradle does, so a library has to publish a js variant built with the IR compiler: JVM-only libraries and libraries built only for the legacy Kotlin/JS compiler can’t be used. The lockfile keeps the resolutions under js/main and js/test. See Multiplatform.

[frameworks] serialization, coroutines and the Ktor client (ktor = { client = "js" }) work on the JS target. Compose for Web runs on Kotlin/Wasm; use [target.wasm] for it. The Ktor server, Lombok and Spring Boot need the JVM. See Frameworks.

File What it is
<name>.mjs or <name>.js The program or library, with the standard library and the other klibs linked in
<name>.mjs.map The source map (dev builds)
<name>.d.mts or <name>.d.ts TypeScript declarations (libraries only)
package.json The npm package (libraries only)
index.html and your resources/ The page of a browser app
node_modules/ The npm packages the module imports
test/ The tests and their runner, after kargo test
  • kargo test --list doesn’t list JS tests yet.
  • The whole program is linked into one file; there is no per-module output or incremental linking yet.
  • Kotlin declarations are not generated from TypeScript for JS (they are for Wasm).
  • kargo run serves a browser app as it is; it doesn’t rebuild or reload the page when the sources change.