diff --git a/README.md b/README.md index d30f247..d036c05 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Kotlin hello world for Cloudflare Workers -Your Kotlin code in [main.kt](https://github.com/cloudflare/kotlin-worker-hello-world/blob/master/src/main/kotlin/main.kt), running on Cloudflare Workers +Your Kotlin code in [main.kt](src/wasmJsMain/kotlin/main.kt), running on Cloudflare Workers -In addition to [Wrangler v2.x](https://github.com/cloudflare/wrangler2) you will need to install Kotlin, including a JDK and support for Gradle projects. The easiest way to do this is using the free Community Edition of [IntelliJ IDEA](https://kotlinlang.org/docs/tutorials/jvm-get-started.html). +In addition to [Wrangler v2.x](https://github.com/cloudflare/wrangler2) you will need to install a JDK 8 or newer. [The easiest way to do this](https://www.jetbrains.com/guide/java/tips/download-jdk/) is using the free Community Edition of [IntelliJ IDEA](https://www.jetbrains.com/idea/download/). ## Wrangler @@ -10,18 +10,22 @@ Configure the [wrangler.toml](wrangler.toml) by filling in the `account_id` from Further documentation for Wrangler can be found [here](https://developers.cloudflare.com/workers/tooling/wrangler). -## Gradle +## Build & Deploy -After setting up Kotlin per the linked instructions above, +After setting up your environment, run the following command: -``` -./gradlew :compileProductionExecutableKotlinJs +```shell +./gradlew :compileProductionExecutableKotlinWasmJs ``` -That will compile your code and package it into a JavaScript executable, after which you can run `wrangler publish` to push it to Cloudflare. +That will compile your code into a WebAssembly executable and JavaScript glue code, +after which you can run `wrangler deploy` to push it to Cloudflare: +```shell +npx wrangler@latest deploy build/js/packages/kotlin-worker-hello-world-wasm-js/kotlin/kotlin-worker-hello-world.js ``` -wrangler publish build/js/packages/kotlin-worker-hello-world/kotlin/kotlin-worker-hello-world.js -``` -For more information on interop between Kotlin and Javascript, see the [Kotlin docs](https://kotlinlang.org/docs/reference/js-interop.html). Regarding coroutines, see [this issue and workaround](https://github.com/cloudflare/kotlin-worker-hello-world/issues/2) +## Learn more + +* [Kotlin/Wasm Overview](https://kotl.in/wasm/) +* [Kotlin/Wasm JavaScript interop](https://kotlinlang.org/docs/wasm-js-interop.html). diff --git a/build.gradle.kts b/build.gradle.kts index e31f80e..3279a5a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,10 @@ +import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension +import org.jetbrains.kotlin.gradle.targets.js.npm.tasks.KotlinNpmInstallTask +import org.jetbrains.kotlin.gradle.targets.js.ir.* + plugins { - kotlin("js") version "1.7.10" + kotlin("multiplatform") version "1.9.22" + // kotlin("multiplatform") version "2.0.0-RC1" } group = "org.example" @@ -10,9 +15,62 @@ repositories { } kotlin { - js(IR) { + wasmJs { nodejs { } binaries.executable() + + // Comment the next line to turn off optimization by Binaryen + applyBinaryen() + + // Workaround for https://youtrack.jetbrains.com/issue/KT-66972 + compilations + .configureEach { + binaries.withType().configureEach { + linkTask.configure { + val moduleName = linkTask.flatMap { + it.compilerOptions.moduleName + } + val fileName = moduleName.map { module -> + "$module.uninstantiated.mjs" + } + + val mjsFile: Provider = linkTask.flatMap { + it.destinationDirectory.file(fileName.get()) + } + + doLast { + val module = moduleName.get() + val file = mjsFile.get().asFile + val text = file.readText() + val newText = text + .replace("if \\(!\\w+( && !\\w+)*\\) \\{[^\\}]*\\}".toRegex(), "") + .replace( + "(if \\(isBrowser\\) \\{\\s*wasmInstance[^\\}]*\\})".toRegex(), + """ + $1 + + const isWorker = navigator.userAgent.includes("Workers") + if (isWorker) { + const { default: wasmModule } = await import('./$module.wasm'); + wasmInstance = (await WebAssembly.instantiate(wasmModule, importObject)); + } + """.trimIndent() + ) + + file.writeText(newText) + } + } + } + } } } + +// To run in Node.js +rootProject.the().apply { + nodeVersion = "22.0.0-nightly2024021536dcd399c0" + nodeDownloadBaseUrl = "https://nodejs.org/download/nightly" +} +tasks.withType().configureEach { + args.add("--ignore-engines") +} diff --git a/src/main/kotlin/main.kt b/src/wasmJsMain/kotlin/main.kt similarity index 60% rename from src/main/kotlin/main.kt rename to src/wasmJsMain/kotlin/main.kt index dd8d0a6..f49acd9 100644 --- a/src/main/kotlin/main.kt +++ b/src/wasmJsMain/kotlin/main.kt @@ -1,3 +1,4 @@ +import org.w3c.fetch.Headers import org.w3c.fetch.Request import org.w3c.fetch.Response import org.w3c.fetch.ResponseInit @@ -5,10 +6,10 @@ import org.w3c.fetch.ResponseInit @OptIn(ExperimentalJsExport::class) @JsExport fun fetch(request: Request) : Response { - val headers: dynamic = object {} - headers["content-type"] = "text/plain" + val headers = Headers() + headers.append("content-type", "text/plain") return Response( - "Kotlin Worker hello world", + "Hello from Kotlin/Wasm Worker".toJsString(), ResponseInit(headers = headers) ) } diff --git a/src/main/resources/index.html b/src/wasmJsMain/resources/index.html similarity index 100% rename from src/main/resources/index.html rename to src/wasmJsMain/resources/index.html diff --git a/src/wasmJsMain/resources/kotlin-worker-hello-world.js b/src/wasmJsMain/resources/kotlin-worker-hello-world.js new file mode 100644 index 0000000..b166f0f --- /dev/null +++ b/src/wasmJsMain/resources/kotlin-worker-hello-world.js @@ -0,0 +1,5 @@ +import * as foo from "./kotlin-worker-hello-world-wasm-js.mjs" + +let keys = Object.keys(foo); + +export default keys.length === 1 && keys[0] === "default" ? { ... foo.default } : foo; \ No newline at end of file