Skip to content

Commit

Permalink
Add very basic Kotlin/JS support: ability to compile the binary (#3678)
Browse files Browse the repository at this point in the history
Another part for #3611.

This PR adds a very basic foundation for the Kotlin/JS support. The code
provided allows to:

* Build code with `org.jetbrains` (most likely) dependencies only
* Run it with Node (no browser support)
* Execute tests (using `kotlinx-test` so far), but without any test
results collection/tests selector, etc.

However, I think that full Kotlin/JS support **will take a lot of
time**, because even if there is a Scala.JS counterpart available, the
way these technologies work is very different (distribution, format,
etc.)

Issues I've encountered:

* Kotlin 2+ is not supported, because with Kotlin 2+ jars are not
published anymore and there is only `klib` file available (see
https://repo1.maven.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-js/2.0.0/).
When Gradle is used, it is able to fetch it using attributes declared in
`.module` file, but Coursier is not able to recognize and fetch it.
* Kotlin versions below 1.8.20 are not supported, because of the
different set of compiler arguments. With the certain effort it is
possible to go further in supporting older versions, but I'm not sure if
it is needed: since Mill is somewhat experimental, probably there is no
need for the users to use old Kotlin versions.
* Even if some Kotlin/JS library has `jar` file published with Kotlin/JS
library inside, it may be rejected by the compiler. For example, this
https://repo1.maven.org/maven2/org/jetbrains/kotlinx/kotlinx-html-js/0.8.0/
has the necessary `jar` file with `.meta.js` / `.kjsm` files inside, but
it is rejected by the compiler of Kotlin 1.8/1.9. Here
https://repo1.maven.org/maven2/org/jetbrains/kotlin/kotlin-stdlib-js/1.9.24/,
for example, nothing is rejected, so I suppose there is an issue in the
ABI/metadata version. Not sure how it can be solved (maybe by relying
only on `klib`? But `klib` cannot be fetched by Coursier).
* Gradle Kotlin plugin is utilizing NPM dependencies to generate
`package.json` and add the necessary JS test frameworks/runners there if
executed in Node environment, or even webpack for Browser environment.
This is also a big chunk of work to be done. For now I've added only
test binary execution, but there is no test results collection / test
selector.
* Kotest cannot be used, because with version 5 only `klib` is
published, and `jar` of version 4 is not compatible with the 1.8/1.9 IR
compiler.
* ~~Kotlin/JS IR compiler has different modes: it can output either
IR/Klib or can produce final JS (basically IR+linking). Kotlin Gradle
plugin is using 2 passes: to generate IR and then produce final JS. I
guess it is done for the better performance / better incremental
support, but I, for the initial drop, rely on a single pass (IR+linking
in a single compiler invocation) => **need to make `compile` task to
produce only IR code and add kind of `link` task to produce executable
in the future.**~~ => addressed in the 2nd commit.

---------

Co-authored-by: 0xnm <[email protected]>
Co-authored-by: Li Haoyi <[email protected]>
  • Loading branch information
3 people authored Oct 7, 2024
1 parent cff9993 commit ebee7a0
Show file tree
Hide file tree
Showing 16 changed files with 1,019 additions and 20 deletions.
5 changes: 5 additions & 0 deletions docs/modules/ROOT/pages/kotlinlib/web-examples.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ It covers setting up a basic backend server with a variety of server frameworks
== Ktor Hello World App

include::partial$example/kotlinlib/web/1-hello-ktor.adoc[]

== Ktor TodoMvc App

include::partial$example/kotlinlib/web/2-todo-ktor.adoc[]

== (Work In Progress) Simple KotlinJS Module

include::partial$example/kotlinlib/web/3-hello-kotlinjs.adoc[]

43 changes: 43 additions & 0 deletions example/kotlinlib/web/3-hello-kotlinjs/build.mill
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// KotlinJS support on Mill is still Work In Progress (WIP). As of time of writing it
// does not support third-party dependencies, Kotlin 2.x with KMP KLIB files, Node.js/Webpack
// test runners and reporting, etc.
//
// The example below demonstrates only the minimal compilation, running, and testing of a single KotlinJS
// module. For more details in fully developing KotlinJS support, see the following ticket:
//
// * https://github.com/com-lihaoyi/mill/issues/3611

package build
import mill._, kotlinlib._, kotlinlib.js._

object foo extends KotlinJSModule {
def moduleKind = ModuleKind.ESModule
def kotlinVersion = "1.9.25"
def kotlinJSRunTarget = Some(RunTarget.Node)
object test extends KotlinJSModule with KotlinJSKotlinXTests
}


/** Usage

> mill foo.run
Compiling 1 Kotlin sources to .../out/foo/compile.dest/classes...
Hello, world
stringifiedJsObject: ["hello","world","!"]

> mill foo.test # Test is incorrect, `foo.test`` fails
Compiling 1 Kotlin sources to .../out/foo/test/compile.dest/classes...
Linking IR to .../out/foo/test/linkBinary.dest/binaries
produce executable: .../out/foo/test/linkBinary.dest/binaries
...
error: AssertionError: Expected <Hello, world>, actual <Not hello, world>.

> cat out/foo/test/linkBinary.dest/binaries/test.js # Generated javascript on disk
...assertEquals_0(getString(), 'Not hello, world');...
...

> sed -i.bak 's/Not hello, world/Hello, world/g' foo/test/src/foo/HelloTests.kt

> mill foo.test # passes after fixing test

*/
11 changes: 11 additions & 0 deletions example/kotlinlib/web/3-hello-kotlinjs/foo/src/foo/Hello.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package foo

fun getString() = "Hello, world"

fun main() {
println(getString())

val parsedJsonStr: dynamic = JSON.parse("""{"helloworld": ["hello", "world", "!"]}""")
val stringifiedJsObject = JSON.stringify(parsedJsonStr.helloworld)
println("stringifiedJsObject: " + stringifiedJsObject)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package foo

import kotlin.test.Test
import kotlin.test.assertEquals

class HelloTests {

@Test
fun failure() {
assertEquals(getString(), "Not hello, world")
}
}

16 changes: 2 additions & 14 deletions kotlinlib/src/mill/kotlinlib/KotlinModule.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ package kotlinlib

import mill.api.{Loose, PathRef, Result}
import mill.define.{Command, ModuleRef, Task}
import mill.kotlinlib.worker.api.KotlinWorker
import mill.kotlinlib.worker.api.{KotlinWorker, KotlinWorkerTarget}
import mill.scalalib.api.{CompilationResult, ZincWorkerApi}
import mill.scalalib.{JavaModule, Lib, ZincWorkerModule}
import mill.util.Jvm
Expand Down Expand Up @@ -92,11 +92,6 @@ trait KotlinModule extends JavaModule { outer =>
*/
def kotlinCompilerIvyDeps: T[Agg[Dep]] = Task {
Agg(ivy"org.jetbrains.kotlin:kotlin-compiler:${kotlinCompilerVersion()}") ++
// (
// if (Seq("1.0.", "1.1.", "1.2").exists(prefix => kotlinVersion().startsWith(prefix)))
// Agg(ivy"org.jetbrains.kotlin:kotlin-runtime:${kotlinCompilerVersion()}")
// else Seq()
// ) ++
(
if (
!Seq("1.0.", "1.1.", "1.2.0", "1.2.1", "1.2.2", "1.2.3", "1.2.4").exists(prefix =>
Expand All @@ -106,15 +101,8 @@ trait KotlinModule extends JavaModule { outer =>
Agg(ivy"org.jetbrains.kotlin:kotlin-scripting-compiler:${kotlinCompilerVersion()}")
else Seq()
)
// ivy"org.jetbrains.kotlin:kotlin-scripting-compiler-impl:${kotlinCompilerVersion()}",
// ivy"org.jetbrains.kotlin:kotlin-scripting-common:${kotlinCompilerVersion()}",
}

// @Deprecated("Use kotlinWorkerTask instead, as this does not need to be cached as Worker")
// def kotlinWorker: Worker[KotlinWorker] = Task.Worker {
// kotlinWorkerTask()
// }

def kotlinWorkerTask: Task[KotlinWorker] = Task.Anon {
kotlinWorkerRef().kotlinWorkerManager().get(kotlinCompilerClasspath())
}
Expand Down Expand Up @@ -264,7 +252,7 @@ trait KotlinModule extends JavaModule { outer =>
(kotlinSourceFiles ++ javaSourceFiles).map(_.toIO.getAbsolutePath())
).flatten

val workerResult = kotlinWorkerTask().compile(compilerArgs: _*)
val workerResult = kotlinWorkerTask().compile(KotlinWorkerTarget.Jvm, compilerArgs: _*)

val analysisFile = dest / "kotlin.analysis.dummy"
os.write(target = analysisFile, data = "", createFolders = true)
Expand Down
Loading

0 comments on commit ebee7a0

Please sign in to comment.