Repository: tipsy/javalin
Pull request #1078 (2020-10-02 15:13:07)

[javalinvue] Add option to inline files and set isDev function


Patch:
diff --git a/javalin/src/main/java/io/javalin/plugin/rendering/vue/JavalinVue.kt b/javalin/src/main/java/io/javalin/plugin/rendering/vue/JavalinVue.kt
index 0a914e26..e412c56b 100644
--- a/javalin/src/main/java/io/javalin/plugin/rendering/vue/JavalinVue.kt
+++ b/javalin/src/main/java/io/javalin/plugin/rendering/vue/JavalinVue.kt
@@ -12,6 +12,7 @@ import io.javalin.http.Handler
 import io.javalin.http.staticfiles.Location
 import io.javalin.http.util.ContextUtil.isLocalhost
 import io.javalin.plugin.json.JavalinJson
+import io.javalin.plugin.rendering.vue.FileInliner.inlineFiles
 import java.nio.file.FileSystems
 import java.nio.file.Files
 import java.nio.file.Path
@@ -20,9 +21,8 @@ import java.util.stream.Collectors
 
 object JavalinVue {
 
-    internal var useCdn = false
-
-    private var vueDirPath: Path? = null
+    internal var isDev: Boolean? = null
+    internal var vueDirPath: Path? = null
 
     @JvmStatic
     fun rootDirectory(path: String, location: Location) {
@@ -40,6 +40,9 @@ object JavalinVue {
     @JvmField
     var cacheControl = "no-cache, no-store, must-revalidate"
 
+    @JvmField
+    var isDevFunction: (Context) -> Boolean = { it.isLocalhost() }
+
     internal fun walkPaths(): Set<Path> = Files.walk(vueDirPath, 10).collect(Collectors.toSet())
 
     internal val cachedPaths by lazy { walkPaths() }
@@ -47,11 +50,12 @@ object JavalinVue {
 
     internal fun createLayout(paths: Set<Path>) = paths
             .find { it.endsWith("vue/layout.html") }!!.readText()
+            .inlineFiles(paths)
             .replace("@componentRegistration", "@componentRegistration@serverState") // add state anchor for later
             .replace("@componentRegistration", paths
                     .filter { it.toString().endsWith(".vue") }
-                    .joinToString("") { "\n<!-- ${it.fileName} -->\n" + it.readText() }
-            ).replaceWebjarsWithCdn()
+                    .joinToString("") { "\n<!-- ${it.fileName} -->\n" + it.readText() })
+            .replaceWebjarsWithCdn()
 
     internal fun getState(ctx: Context, state: Any?) = "\n<script>\n" + """
         |    Vue.prototype.${"$"}javalin = {
@@ -60,22 +64,19 @@ object JavalinVue {
         |        state: ${JavalinJson.toJson(state ?: stateFunction(ctx))}
         |    }""".trimMargin() + "\n</script>\n"
 
-    internal fun setRootDirPathIfUnset(ctx: Context) {
-        vueDirPath = vueDirPath ?: if (ctx.isLocalhost()) Paths.get("src/main/resources/vue") else PathMaster.classpathPath("/vue")
-    }
-
     private fun String.replaceWebjarsWithCdn() =
-            this.replace("@cdnWebjar/", if (useCdn) "https://cdn.jsdelivr.net/webjars/org.webjars.npm/" else "/webjars/")
+            this.replace("@cdnWebjar/", if (isDev == true) "/webjars/" else "https://cdn.jsdelivr.net/webjars/org.webjars.npm/")
 
 }
 
+
 class VueComponent @JvmOverloads constructor(private val component: String, private val state: Any? = null) : Handler {
     override fun handle(ctx: Context) {
-        JavalinVue.setRootDirPathIfUnset(ctx)
-        JavalinVue.useCdn = !ctx.isLocalhost()
+        JavalinVue.isDev = JavalinVue.isDev ?: JavalinVue.isDevFunction(ctx)
+        JavalinVue.vueDirPath = JavalinVue.vueDirPath ?: PathMaster.defaultLocation(JavalinVue.isDev)
         val routeComponent = if (component.startsWith("<")) component else "<$component></$component>"
-        val paths = if (ctx.isLocalhost()) JavalinVue.walkPaths() else JavalinVue.cachedPaths
-        val view = if (ctx.isLocalhost()) JavalinVue.createLayout(paths) else JavalinVue.cachedLayout
+        val paths = if (JavalinVue.isDev == true) JavalinVue.walkPaths() else JavalinVue.cachedPaths
+        val view = if (JavalinVue.isDev == true) JavalinVue.createLayout(paths) else JavalinVue.cachedLayout
         val componentName = routeComponent.removePrefix("<").takeWhile { it !in setOf('>', ' ') }
         if (!view.contains(componentName)) {
             ctx.result("Route component not found: $routeComponent")
@@ -100,6 +101,30 @@ object PathMaster {
         PathMaster::class.java.getResource(path).toURI().scheme == "jar" -> fileSystem.getPath(path) // we're inside a jar
         else -> Paths.get(PathMaster::class.java.getResource(path).toURI()) // we're not in jar (probably running from IDE)
     }
+
+    fun defaultLocation(isDev: Boolean?) = if (isDev == true) Paths.get("src/main/resources/vue") else classpathPath("/vue")
+}
+
+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(paths: Set<Path>): String {
+        val pathMap = paths.filterNot { it.toString().endsWith(".vue") } // vue files are inlined in @componentRegistration later
+                .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 { pathMap[matchingKey]!!.readText() }
+            when {
+                devRegex.containsMatchIn(line) -> if (JavalinVue.isDev == true) line.replace(devRegex, matchingFileContent) else ""
+                notDevRegex.containsMatchIn(line) -> if (JavalinVue.isDev == false) line.replace(notDevRegex, matchingFileContent) else ""
+                else -> line.replace(unconditionalRegex, matchingFileContent)
+            }
+        }
+    }
 }
 
 fun Path.readText() = String(Files.readAllBytes(this))


Test patch:
diff --git a/javalin/src/test/java/io/javalin/TestJavalinVue.kt b/javalin/src/test/java/io/javalin/TestJavalinVue.kt
index 0bc84a3c..e309f290 100644
--- a/javalin/src/test/java/io/javalin/TestJavalinVue.kt
+++ b/javalin/src/test/java/io/javalin/TestJavalinVue.kt
@@ -16,13 +16,17 @@ import io.mockk.mockk
 import io.mockk.slot
 import io.mockk.verify
 import org.assertj.core.api.Assertions.assertThat
+import org.junit.Before
 import org.junit.Test
 import java.nio.file.Paths
 
 class TestJavalinVue {
 
-    init {
-        JavalinVue.rootDirectory("src/test/resources/vue", Location.EXTERNAL)
+    @Before
+    fun setup() {
+        JavalinVue.isDev = null // reset
+        JavalinVue.stateFunction = { ctx -> mapOf<String, String>() } // reset
+        JavalinVue.rootDirectory("src/test/resources/vue", Location.EXTERNAL) // src/main -> src/test
     }
 
     data class User(val name: String, val email: String)
@@ -48,7 +52,6 @@ class TestJavalinVue {
 
     @Test
     fun `vue component without state`() = TestUtil.test { app, http ->
-        JavalinVue.stateFunction = { ctx -> mapOf<String, String>() }
         app.get("/no-state", VueComponent("<test-component></test-component>"))
         val res = http.getBody("/no-state")
         assertThat(res).contains("""pathParams: {}""")
@@ -59,7 +62,6 @@ class TestJavalinVue {
 
     @Test
     fun `vue component with component-specific state`() = TestUtil.test { app, http ->
-        JavalinVue.stateFunction = { ctx -> mapOf<String, String>() }
         app.get("/no-state", VueComponent("<test-component></test-component>"))
         val noStateRes = http.getBody("/no-state")
         app.get("/specific-state", VueComponent("<test-component></test-component>", mapOf("test" to "tast")))
@@ -112,11 +114,10 @@ class TestJavalinVue {
     }
 
     @Test
-    fun `classpath works`() = TestUtil.test { app, http ->
+    fun `classpath rootDirectory works`() = TestUtil.test { app, http ->
         JavalinVue.rootDirectory("/vue", Location.CLASSPATH)
         app.get("/classpath", VueComponent("test-component"))
         assertThat(http.getBody("/classpath")).contains("<test-component></test-component>")
-        JavalinVue.rootDirectory("src/test/resources/vue", Location.EXTERNAL)
     }
 
     @Test
@@ -124,7 +125,6 @@ class TestJavalinVue {
         JavalinVue.rootDirectory(Paths.get("src/test/resources/vue"))
         app.get("/path", VueComponent("test-component"))
         assertThat(http.getBody("/path")).contains("<test-component></test-component>")
-        JavalinVue.rootDirectory("src/test/resources/vue", Location.EXTERNAL)
     }
 
     @Test
@@ -132,7 +132,6 @@ class TestJavalinVue {
         JavalinVue.rootDirectory("/vue", Location.EXTERNAL)
         app.get("/fail", VueComponent("test-component"))
         assertThat(http.get("/fail").status).isEqualTo(500)
-        JavalinVue.rootDirectory("src/test/resources/vue", Location.EXTERNAL)
     }
 
     @Test
@@ -162,4 +161,30 @@ class TestJavalinVue {
         assertThat(slot.captured).contains("""src="https://cdn.jsdelivr.net/webjars/""")
     }
 
+    @Test
+    fun `@inlineFile functionality works as expected if not-dev`() = TestUtil.test { app, http ->
+        val ctx = mockk<Context>(relaxed = true)
+        every { ctx.url() } returns "http://123.123.123.123:1234/"
+        VueComponent("<test-component></test-component>").handle(ctx)
+        val slot = slot<String>().also { verify { ctx.html(html = capture(it)) } }
+        assertThat(slot.captured).contains("""<script>let a = "Always included"</script>""")
+        assertThat(slot.captured).contains("""<script>let b = "Included if not dev"</script>""")
+        assertThat(slot.captured).doesNotContain("""<script>let b = "Included if dev"</script>""")
+        assertThat(slot.captured).doesNotContain("""<script>@inlineFileDev("/vue/scripts-dev.js")</script>""")
+        assertThat(slot.captured).doesNotContain("""<script>@inlineFile""")
+    }
+
+    @Test
+    fun `@inlineFile functionality works as expected if dev`() = TestUtil.test { app, http ->
+        val ctx = mockk<Context>(relaxed = true)
+        every { ctx.url() } returns "http://localhost:1234/"
+        VueComponent("<test-component></test-component>").handle(ctx)
+        val slot = slot<String>().also { verify { ctx.html(html = capture(it)) } }
+        assertThat(slot.captured).contains("""<script>let a = "Always included"</script>""")
+        assertThat(slot.captured).contains("""<script>let b = "Included if dev"</script>""")
+        assertThat(slot.captured).doesNotContain("""<script>let b = "Included if not dev"</script>""")
+        assertThat(slot.captured).doesNotContain("""<script>@inlineFileNotDev("/vue/scripts-not-dev.js")</script>""")
+        assertThat(slot.captured).doesNotContain("""<script>@inlineFile""")
+    }
+
 }
diff --git a/javalin/src/test/resources/vue/layout.html b/javalin/src/test/resources/vue/layout.html
index af638643..a4364762 100644
--- a/javalin/src/test/resources/vue/layout.html
+++ b/javalin/src/test/resources/vue/layout.html
@@ -1,5 +1,8 @@
 <head>
     @componentRegistration
+    <script>@inlineFile("/vue/scripts.js")</script>
+    <script>@inlineFileDev("/vue/scripts-dev.js")</script>
+    <script>@inlineFileNotDev("/vue/scripts-not-dev.js")</script>
     <script src="@cdnWebjar/swagger-ui/3.24.3/swagger-ui.css"></script>
 </head>
 <body>@routeComponent</body>
diff --git a/javalin/src/test/resources/vue/scripts-dev.js b/javalin/src/test/resources/vue/scripts-dev.js
new file mode 100644
index 00000000..543dc5f6
--- /dev/null
+++ b/javalin/src/test/resources/vue/scripts-dev.js
@@ -0,0 +1,1 @@
+let b = "Included if dev"
\ No newline at end of file
diff --git a/javalin/src/test/resources/vue/scripts-not-dev.js b/javalin/src/test/resources/vue/scripts-not-dev.js
new file mode 100644
index 00000000..57d8dd62
--- /dev/null
+++ b/javalin/src/test/resources/vue/scripts-not-dev.js
@@ -0,0 +1,1 @@
+let b = "Included if not dev"
\ No newline at end of file
diff --git a/javalin/src/test/resources/vue/scripts.js b/javalin/src/test/resources/vue/scripts.js
new file mode 100644
index 00000000..edc73939
--- /dev/null
+++ b/javalin/src/test/resources/vue/scripts.js
@@ -0,0 +1,1 @@
+let a = "Always included"
\ No newline at end of file