All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.javalin.plugin.rendering.vue.VueHandler.kt Maven / Gradle / Ivy

package io.javalin.plugin.rendering.vue

import io.javalin.core.util.Header
import io.javalin.http.Context
import io.javalin.http.Handler
import io.javalin.http.InternalServerErrorResponse
import io.javalin.plugin.json.jsonMapper
import io.javalin.plugin.rendering.vue.FileInliner.inlineFiles
import io.javalin.plugin.rendering.vue.JavalinVue.cacheControl
import io.javalin.plugin.rendering.vue.JavalinVue.cachedDependencyResolver
import io.javalin.plugin.rendering.vue.JavalinVue.cachedPaths
import io.javalin.plugin.rendering.vue.JavalinVue.isDev
import io.javalin.plugin.rendering.vue.JavalinVue.isDevFunction
import io.javalin.plugin.rendering.vue.JavalinVue.optimizeDependencies
import io.javalin.plugin.rendering.vue.JavalinVue.rootDirectory
import io.javalin.plugin.rendering.vue.JavalinVue.stateFunction
import io.javalin.plugin.rendering.vue.JavalinVue.walkPaths
import java.net.URLEncoder
import java.nio.file.Files
import java.nio.file.Path
import java.util.regex.Matcher

abstract class VueHandler(private val componentId: String) : Handler {

    open fun state(ctx: Context): Any? = null
    open fun preRender(layout: String, ctx: Context): String = layout
    open fun postRender(layout: String, ctx: Context): String = layout

    override fun handle(ctx: Context) {
        isDev = isDev ?: isDevFunction(ctx)
        rootDirectory = rootDirectory ?: PathMaster.defaultLocation(isDev)
        val routeComponent = if (componentId.startsWith("<")) componentId else "<$componentId>"
        val allFiles = if (isDev == true) walkPaths() else cachedPaths
        val resolver by lazy { if (isDev == true) VueDependencyResolver(allFiles, JavalinVue.vueAppName) else cachedDependencyResolver }
        val componentId = routeComponent.removePrefix("<").takeWhile { it !in setOf('>', ' ') }
        val dependencies = if (optimizeDependencies) resolver.resolve(componentId) else allFiles.joinVueFiles()
        if (componentId !in dependencies) throw InternalServerErrorResponse("Route component not found: $routeComponent")
        ctx.html(
            allFiles.find { it.endsWith("vue/layout.html") }!!.readText() // we start with the layout file
                .preRenderHook(ctx)
                .inlineFiles(allFiles.filterNot { it.isVueFile() }) // we then inline css/js files
                .replace("@componentRegistration", "@loadableData@componentRegistration@serverState") // add anchors for later
                .replace("@loadableData", loadableDataScript) // add loadable data class
                .replace("@componentRegistration", dependencies) // add all dependencies
                .replace("@serverState", getState(ctx, state(ctx))) // add escaped params and state
                .replace("@routeComponent", routeComponent) // finally, add the route component itself
                .replace("@cdnWebjar/", if (isDev == true) "/webjars/" else "https://cdn.jsdelivr.net/webjars/org.webjars.npm/")
                .postRenderHook(ctx)
        ).header(Header.CACHE_CONTROL, cacheControl)
    }

    private fun String.preRenderHook(ctx: Context) = preRender(this, ctx);
    private fun String.postRenderHook(ctx: Context) = postRender(this, ctx);
}


private fun Set.joinVueFiles() = this.filter { it.isVueFile() }.joinToString("") { "\n\n" + it.readText() }

object FileInliner {
    private val newlineRegex = Regex("\\r?\\n")
    private val unconditionalRegex = Regex("""@inlineFile\(".*"\)""")
    private val devRegex = Regex("""@inlineFileDev\(".*"\)""")
    private val notDevRegex = Regex("""@inlineFileNotDev\(".*"\)""")

    fun String.inlineFiles(nonVueFiles: List): String {
        val pathMap = nonVueFiles.associateBy { """"/vue/${it.toString().replace("\\", "/").substringAfter("/vue/")}"""" } // normalize keys
        return this.split(newlineRegex).joinToString("\n") { line ->
            if (!line.contains("@inlineFile")) return@joinToString line // nothing to inline
            val matchingKey = pathMap.keys.find { line.contains(it) } ?: throw IllegalStateException("Invalid path found: $line")
            val matchingFileContent by lazy { Matcher.quoteReplacement(pathMap[matchingKey]!!.readText()) }
            when {
                devRegex.containsMatchIn(line) -> if (isDev == true) line.replace(devRegex, matchingFileContent) else ""
                notDevRegex.containsMatchIn(line) -> if (isDev == false) line.replace(notDevRegex, matchingFileContent) else ""
                else -> line.replace(unconditionalRegex, matchingFileContent)
            }
        }
    }
}

internal fun getState(ctx: Context, state: Any?) = "\n\n"

// Unfortunately, Java's URLEncoder does not encode the space character in the same way as Javascript.
// Javascript expects a space character to be encoded as "%20", whereas Java encodes it as "+".
// All other encodings are implemented correctly, therefore we can simply replace the character in the encoded String.
private fun urlEncodeForJavascript(string: String) = URLEncoder.encode(string, Charsets.UTF_8.name()).replace("+", "%20")
private fun prototypeOrGlobalConfig() = if (JavalinVue.vueVersion == VueVersion.VUE_3) "${JavalinVue.vueAppName}.config.globalProperties" else "${JavalinVue.vueAppName}.prototype"
internal fun Path.readText() = String(Files.readAllBytes(this))
internal fun Path.isVueFile() = this.toString().endsWith(".vue")




© 2015 - 2024 Weber Informatics LLC | Privacy Policy