Is there an example of serving a Compose Web wasm/...
# compose-web
c
Is there an example of serving a Compose Web wasm/js application with a server, ideally where the client application and server are in separate modules? Most of the examples I’ve found are using the development server, and I’m trying to set up something a bit more realistic. I’m specifically struggling with making sure I’m configuring the HTML file to correctly embed the Compose app, and creating the Gradle configuration for the server app to get the Compose build outputs. I have all of this working for Compose Web HTML, which is helpful but there are some differences I’m trying to sort out.
a
We have an example how to serve a compose web application on GitHub Pages: https://github.com/Kotlin/kotlin-wasm-compose-template/blob/main/.github/workflows/ci.yml
It might be helpful
c
Thank you, that does have a few pieces that answer some questions I have especially on what the HTML needs to contain. So that’ll help me make some progress.
o
Built for a different purpose (supplementing a KotlinConf talk), but could that help? Client html Ktor server application
c
Artem, this is what I ended up implementing. It isn’t perfect and has a few rough edges. Basically, I needed a Gradle task and configuration in the compose client-app module so that the ktor server-app module could consume and receive the wasmJs build outputs. I also needed to move the wasmJs to assets/wasmJs to simplify the security policies around read access for http requests in the server client-app/build.gradle.kts
Copy code
val clientAggregationTask = tasks.register("clientAppAssetsAggregation") {
    dependsOn("wasmJsBrowserDistribution")
    inputs.files(tasks.named("wasmJsBrowserDistribution"))

    val outputDir = layout.buildDirectory.dir("client-app-assets").get().asFile
    outputs.dir(outputDir)

    doLast {
        outputDir.deleteRecursively()
        outputDir.mkdirs()

        inputs.files
            .filter { it.isDirectory }
            .forEach { dir ->
                dir.listFiles()
                    ?.filter { it.isFile }
                    ?.filter { it.extension == "js" || it.extension == "wasm" || it.extension == "mjs" || it.extension == "map" }
                    ?.forEach { file ->
                        file.copyTo(outputDir.resolve("asset/wasmJs/${file.name}"))
                    }

                dir.listFiles()
                    ?.filter { it.isDirectory }
                    ?.filter { it.name == "composeResources" }
                    ?.forEach { composeResourceDir ->
                        composeResourceDir.copyRecursively(outputDir.resolve("asset/wasmJs/${composeResourceDir.name}"))
                    }
            }
    }
}

configurations.register("clientAppProduction") {
    isCanBeConsumed = true
    isCanBeResolved = false
}.also { configuration ->
    if (project.property("SIGMA_IS_WASMJS_ENABLED").toString().toBoolean()) {
        artifacts {
            val resolvedAggregationTask = clientAggregationTask.get()
            add(configuration.name, resolvedAggregationTask.outputs.files.single()) {
                builtBy(resolvedAggregationTask)
            }
        }
    }
}
client-app/src/commonMain/kotlin/Entrypoint.kt
Copy code
fun wasmJsApp() {
    CanvasBasedWindow(canvasElementId = SharedIds.CANVAS_ID) {
        configureWebResources {
            resourcePathMapping { path -> "./asset/wasmJs/$path" }
        }
        App()
    }
}
// server-app/build.gradle.kts
Copy code
kotlin {
    sourceSets {
        getByName("jvmMain") {
            dependencies {
                implementation(
                    project(
                        mapOf(
                            "path" to ":client-app",
                            "configuration" to "clientAppProduction"
                        )
                    )
                )
            }
        }
    }
}
// server-app/src/jvmMain/kotlin/Application.kt
Copy code
fun Routing.registerResourceRoutes() {
    staticResources("asset/wasmJs", "asset/wasmJs")
    
    // Corner case; can't change path for this file
    route("client-app.wasm") {
        get {
            call.resolveResource("asset/wasmJs/client-app.wasm")?.let { resource ->
                call.respond(resource)
            }
        }
    }
}
The main rough edges are: • The app is looking for client-app.wasm in the server’s root directory; I’d like to be able to change the path • Difficulty adding cache busting hashes to files. I generate a synthetic path on the server with the SHA256 of each file, but I don’t have a good way of managing the path re-writes for the skiko.js, skiko.wasm, client-app.wasm files since those are embedded in the JS files by the compiler. This feature request shows an example of the server-side implementation that I have