Skip to content

Commit

Permalink
SSR support
Browse files Browse the repository at this point in the history
  • Loading branch information
rjaros committed Mar 16, 2024
1 parent e2e011e commit 54ff55a
Show file tree
Hide file tree
Showing 35 changed files with 473 additions and 16 deletions.
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ nmcp {
project(":modules:kilua-select-remote")
project(":modules:kilua-splitjs")
project(":modules:kilua-ssr")
project(":modules:kilua-ssr-server")
project(":modules:kilua-ssr-server-ktor")
project(":modules:kilua-tabulator")
project(":modules:kilua-tabulator-remote")
project(":modules:kilua-tempus-dominus")
Expand Down
1 change: 1 addition & 0 deletions kilua/src/commonMain/kotlin/dev/kilua/CoreModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,6 @@ public expect fun initializeCoreModule()
public object CoreModule : ModuleInitializer {
override fun initialize() {
initializeCoreModule()
CssRegister.register("zzz-kilua-assets/style.css")
}
}
40 changes: 40 additions & 0 deletions kilua/src/commonMain/kotlin/dev/kilua/CssRegister.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright (c) 2024 Robert Jaros
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

package dev.kilua

import dev.kilua.utils.nativeListOf

/**
* Kilua CSS register.
*/
public object CssRegister {

public val cssFiles: MutableList<String> = nativeListOf()

/**
* Register CSS file used by the Kilua Modules.
*/
public fun register(cssFile: String) {
cssFiles.add(cssFile)
}
}
4 changes: 4 additions & 0 deletions kilua/src/commonMain/kotlin/dev/kilua/compose/Root.kt
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,10 @@ internal fun rootComposable(
parent = recomposer
)

if (root.renderConfig.isDom) {
// Clear SSR data before rendering
root.node.clear()
}
composition.setContent @Composable {
content(root)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import web.dom.get
* Base class for all components.
*/
public abstract class ComponentBase(
protected val node: Node,
public val node: Node,
public val renderConfig: RenderConfig,
) : Component, PropertyDelegate(nativeMapOf(), skipUpdates = !renderConfig.isDom || !isDom) {

Expand Down
5 changes: 5 additions & 0 deletions kilua/src/commonMain/kotlin/dev/kilua/externals/Object.kt
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,8 @@ public expect fun jsTypeOf(o: JsAny?): String
* Return undefined value
*/
public expect fun undefined(): JsAny?

/**
* JavaScript global object
*/
public external val globalThis: JsAny
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import dev.kilua.utils.JsNonModule
import dev.kilua.utils.useModule
import web.JsAny

@JsModule("bootstrap-icons/font/bootstrap-icons.css")
@JsModule("bootstrap-icons/font/bootstrap-icons.min.css")
@JsNonModule
internal external object BootstrapIconsCss : JsAny

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,6 @@ public object BootstrapCssModule : ModuleInitializer {

override fun initialize() {
useModule(BootstrapCss)
CssRegister.register("bootstrap/dist/css/bootstrap.min.css")
}
}
41 changes: 41 additions & 0 deletions modules/kilua-ssr-server-ktor/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
plugins {
kotlin("multiplatform")
alias(libs.plugins.kotlinx.serialization)
alias(libs.plugins.detekt)
alias(libs.plugins.dokka)
alias(libs.plugins.nmcp)
id("maven-publish")
id("signing")
}

detekt {
toolVersion = libs.versions.detekt.get()
config.setFrom("../../detekt-config.yml")
buildUponDefaultConfig = true
}

kotlin {
explicitApi()
compilerOptions()
kotlinJvmTargets()
sourceSets {
val jvmMain by getting {
dependencies {
implementation(project(":modules:kilua-ssr-server"))
api(libs.ktor.server.core)
}
}
}
}

tasks.register<Jar>("javadocJar") {
dependsOn(tasks.dokkaHtml)
from(tasks.dokkaHtml.flatMap { it.outputDirectory })
archiveClassifier.set("javadoc")
}

setupPublishing()

nmcp {
publishAllPublications {}
}
1 change: 1 addition & 0 deletions modules/kilua-ssr-server-ktor/karma.config.d/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
delete config.webpack.optimization;
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright (c) 2024 Robert Jaros
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

package dev.kilua.ssr

import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.http.content.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.util.*
import io.ktor.util.pipeline.*
import java.util.*

internal val ssrEngineKey: AttributeKey<SsrEngine> = AttributeKey("ssrEngine")

/**
* Initialization function for Kilua Server-Side Rendering.
*/
public fun Application.initSsr() {
val nodeExecutable = environment.config.propertyOrNull("ssr.nodeExecutable")?.getString()
val port = environment.config.propertyOrNull("ssr.port")?.getString()?.toIntOrNull()
val externalSsrService = environment.config.propertyOrNull("ssr.externalSsrService")?.getString()
val rpcUrlPrefix = environment.config.propertyOrNull("ssr.rpcUrlPrefix")?.getString()
val rootId = environment.config.propertyOrNull("ssr.rootId")?.getString() ?: "root"
val ssrEngine = SsrEngine(nodeExecutable, port, externalSsrService, rpcUrlPrefix, rootId)
attributes.put(ssrEngineKey, ssrEngine)
routing {
get("/index.html") {
respondSsr()
}
singlePageApplication {
defaultPage = UUID.randomUUID().toString() // Non-existing resource
filesPath = "/assets"
useResources = true
}
route("/") {
route("{static-content-path-parameter...}") {// Important name from Ktor sources!
get {
respondSsr(call.request.uri)
}
}
}
}
}

private suspend fun PipelineContext<Unit, ApplicationCall>.respondSsr(uri: String = "/") {
if (uri == "/favicon.ico") {
call.respond(HttpStatusCode.NotFound)
} else {
val ssrEngine = call.application.attributes[ssrEngineKey]
call.respondText(ContentType.Text.Html, HttpStatusCode.OK) {
ssrEngine.getSsrContent(uri)
}
}
}
42 changes: 42 additions & 0 deletions modules/kilua-ssr-server/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
plugins {
kotlin("multiplatform")
alias(libs.plugins.kotlinx.serialization)
alias(libs.plugins.detekt)
alias(libs.plugins.dokka)
alias(libs.plugins.nmcp)
id("maven-publish")
id("signing")
}

detekt {
toolVersion = libs.versions.detekt.get()
config.setFrom("../../detekt-config.yml")
buildUponDefaultConfig = true
}

kotlin {
explicitApi()
compilerOptions()
kotlinJvmTargets()
sourceSets {
val jvmMain by getting {
dependencies {
api(libs.ktor.client.core)
api(libs.ktor.client.apache)
api(libs.logback.classic)
}
}
}
}

tasks.register<Jar>("javadocJar") {
dependsOn(tasks.dokkaHtml)
from(tasks.dokkaHtml.flatMap { it.outputDirectory })
archiveClassifier.set("javadoc")
}

setupPublishing()

nmcp {
publishAllPublications {}
}
1 change: 1 addition & 0 deletions modules/kilua-ssr-server/karma.config.d/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
delete config.webpack.optimization;
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Copyright (c) 2024 Robert Jaros
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

package dev.kilua.ssr

import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream

/**
* Converted from https://github.com/eugenp/tutorials/blob/master/core-java-modules/core-java-io/src/main/java/com/baeldung/unzip/UnzipFile.java
*/
internal object FileUtils {
@Throws(IOException::class)
@JvmStatic
fun unzip(inputStream: InputStream, destinationDir: File) {
val buffer = ByteArray(1024)
val zis = ZipInputStream(inputStream)
var zipEntry = zis.nextEntry
while (zipEntry != null) {
val newFile = newFile(destinationDir, zipEntry)
if (zipEntry.isDirectory) {
if (!newFile.isDirectory && !newFile.mkdirs()) {
throw IOException("Failed to create directory $newFile")
}
} else {
val parent = newFile.parentFile
if (!parent.isDirectory && !parent.mkdirs()) {
throw IOException("Failed to create directory $parent")
}

val fos = FileOutputStream(newFile)
var len: Int
while ((zis.read(buffer).also { len = it }) > 0) {
fos.write(buffer, 0, len)
}
fos.close()
}
zipEntry = zis.nextEntry
}
zis.closeEntry()
zis.close()
}

@Throws(IOException::class)
private fun newFile(destinationDir: File, zipEntry: ZipEntry): File {
val destFile = File(destinationDir, zipEntry.name)

val destDirPath = destinationDir.canonicalPath
val destFilePath = destFile.canonicalPath

if (!destFilePath.startsWith(destDirPath + File.separator)) {
throw IOException("Entry is outside of the target dir: " + zipEntry.name)
}

return destFile
}
}
Loading

0 comments on commit 54ff55a

Please sign in to comment.