Repository: tipsy/javalin
Pull request #1075 (2020-09-27 19:24:52)

Added a Class that resolves compnent dependencies for JavalinVue 
This solves  #1071. What was basically done is that i created a class that handles depndency resolution and caching for javalin vue components. This is done by recursively going into each dependency based on tags, and looking into a cached map of .vue files and component names for the required files. Everything is done implicitly, with the restrictions that 

1. 2 Components cant have the same name(which is already a restriction in vue)
2. Files can only contain 1 component(Which is a best practice anyway). 

the second point may be an issue and may need fixing, but i dont see why 2 components would exist in a single .vue file anyway. 

@tipsy please give me your feedback and tell me if this is good. I havent written tests yet, but the current tests dont fail and ive tested manually.

tipsy: > @tipsy please give me your feedback and tell me if this is good. I havent written tests yet, but the current tests dont fail and ive tested manually.

Looks great so far! It's pretty tricky code though, so we need to get tests up and running. You can create a separate file, `TestJavalinVueResolver.java` if you prefer writing tests in Java.

> Files can only contain 1 component(Which is a best practice anyway).
> the second point may be an issue and may need fixing, but i dont see why 2 components would exist in a single .vue file anyway.

Laziness! I've abused this many times myself, for example when making a set of grid components, I declared basically the whole grid in the same file. 

Another thing this will break is people using `.vue` files for inline CSS and JavaScript code. In most my JavalinVue projects I abuse this to avoid loading more external files. This would be better solved by something like `@inlineFile("main.css")`, but that doesn't exist at the moment. 

So, to avoid breaking people's project, I think we need to introduce this change with a config-option, like `JavlalinVue.onlyIncludeRequiredComponents`, which is `false` by deault.


TareqK: @tipsy  a feature flag sounds like a good idea ; I'll patch it in so that the resolver either actually does resolution or I just simply skip that and bundle all. The files.

As for multi component files, I might end up with some dead weight, but I think that if it happens and more than one component exists in a file, it's useless outside the context of that file.

BTW, style and what not are actually loaded, since I load the whole file, and not just the component . 
tipsy: > BTW, style and what not are actually loaded, since I load the whole file, and not just the component .

I typically create something like `_inline-styles.vue`, which only contains:

```html
<style>
    ...
</style>
```

So, no Vue component. But that's not an issue if we create a config-option :)
TareqK: @tipsy  have a look at  the code now; ive added in the tests and ive made the dependency resolution optional(but i do precalculate the cache, but in all cases, when on localhost i re-resolve all dependencies)

tipsy: > @tipsy have a look at the code now; ive added in the tests and ive made the dependency resolution optional(but i do precalculate the cache, but in all cases, when on localhost i re-resolve all dependencies).

> Dunno why macOS is failing the tests, i dont think there are any issues with it since it works OK on both windows and linux, and the single page tests also work; any ideas? I mean nothing has regressed, but why are they failing?

Looks good! I can try running it on my mac in the weekend and see if I can figure it out.
TareqK: Ive added strict runtime checks on inputs, still no luck running on MacOS. There seems to also be  another bug unrelated to this fix in websockets
tipsy: @TareqK I created a function for inlining files to help with this PR (to fix the issue I mentioned about people relying on `.vue` files to simply import CSS/JS). Please have a look: https://github.com/tipsy/javalin/pull/1078
TareqK: @tipsy  Looks good, but which should we merge in first? I dont mind integrating after we merge one or the other.
tipsy: I don't want to merge this one until the tests pass, so I'll do the other one first.
TareqK: @tipsy Then once its merged in ill re-integrate. When can we see the MacOS issue?

tipsy: I'll merge now, then you can rebase, and I will have a look on my Mac tomorrow !
tipsy: Okay, merged! Will have a look at this tomorrow after it's been rebased :)
TareqK: Good news - Merging in @tipsy 's changes broke the build on all platforms - this means i can finally debug
TareqK: Ok so it seems like a super strange error, and I cant pinpoint what is causing it, but it seems that the isDev function ( and perhaps) the isLocalhost() functions have issues on macOS. changing the tests to give a proper value for isDev seems to have fixed the issue as per commit 8e42664 , which is super strange and might need further investigation. @tipsy  have you ever seen something like this ? 
tipsy: I refactored your fork a bit to make it easier to understand for myself @TareqK: now the resolver is only used if it's required (otherwise we use the same string-replace code that has been in `JavalinVue` since the beginning). I'll have a closer look at the code tomorrow.
TareqK: @tipsy  works fine for me , I'll see if I can also do some cleanups tommorow on the resolver 
tipsy: (The changes are at https://github.com/TareqK/javalin/pull/1 in case you didn't see them)
TareqK: @tipsy everything looks good to me; thanks for the cleanup.

BTW, when is the next release out ?
tipsy: > BTW, when is the next release out ? 

Pretty soon after this is merged :)
tipsy: @TareqK added another test and did some more refactoring: https://github.com/TareqK/javalin/pull/2
tipsy: I wish the `resolve` method didn't mutate its argument, do you think you could rewrite that?
TareqK: @tipsy  I dont see where there is mutation, to verify, i made all arguments finalized to make sure that nothing spooky is going on. Anyway, your changes have been added in again, and i verified everything is fine.
tipsy: In general it's considered an antipattern to take a collection as an argument and mutate its contents. This:

```java
Set<String> dependencies = new HashSet<>();
resolve(componentId, dependencies);
```

should (in my opinion) be 

```java
Set<String> dependencies = buildAndCacheDependencies(componentId);
```

This will make the code easier to reason about.

tipsy: In general it's considered an antipattern to take a collection as an argument and mutate its contents. This:

```java
Set<String> dependencies = new HashSet<>();
resolve(componentId, dependencies);
```

should (in my opinion) be 

```java
Set<String> dependencies = getTransitiveDendencies(componentId);
```

This will make the code easier to reason about.

TareqK: @tipsy  Ok now i get what you mean. Yeah i've gone ahead and patched it. Coffee helped  alot :D 
tipsy: Perfect, great job !

Are you ready for merge, or do you want to make some more adjustments?
TareqK: @tipsy Im ready from my side, I just did a quick rename of a method to make the code cleaner. Once the tests pass, merge me in !
TareqK: @tipsy  You can merge this in now, we're all clear

tipsy: Great, thank you 🎉 

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 e412c56b..a38a2ff7 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
@@ -13,6 +13,7 @@ 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 io.javalin.plugin.rendering.vue.JavalinVue.getAllDependencies
 import java.nio.file.FileSystems
 import java.nio.file.Files
 import java.nio.file.Path
@@ -34,6 +35,9 @@ object JavalinVue {
         vueDirPath = path
     }
 
+    @JvmField
+    var optimizeDependencies = false
+
     @JvmField
     var stateFunction: (Context) -> Any = { mapOf<String, String>() }
 
@@ -46,16 +50,18 @@ object JavalinVue {
     internal fun walkPaths(): Set<Path> = Files.walk(vueDirPath, 10).collect(Collectors.toSet())
 
     internal val cachedPaths by lazy { walkPaths() }
-    internal val cachedLayout by lazy { createLayout(cachedPaths) }
+    internal val cachedDependencyResolver by lazy { VueDependencyResolver(cachedPaths) }
+
+    internal fun createLayout(paths: Set<Path>, componentDependencies: String): String {
+        return paths.find { it.endsWith("vue/layout.html") }!!.readText()
+                .inlineFiles(paths)
+                .replace("@componentRegistration", "@componentRegistration@serverState") // add state anchor for later
+                .replace("@componentRegistration", componentDependencies)
+                .replaceWebjarsWithCdn()
+    }
 
-    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()
+    internal fun getAllDependencies(paths: Set<Path>) = paths.filter { it.isVueFile() }
+            .joinToString("") { "\n<!-- ${it.fileName} -->\n" + it.readText() }
 
     internal fun getState(ctx: Context, state: Any?) = "\n<script>\n" + """
         |    Vue.prototype.${"$"}javalin = {
@@ -76,9 +82,10 @@ class VueComponent @JvmOverloads constructor(private val component: String, priv
         JavalinVue.vueDirPath = JavalinVue.vueDirPath ?: PathMaster.defaultLocation(JavalinVue.isDev)
         val routeComponent = if (component.startsWith("<")) component else "<$component></$component>"
         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)) {
+        val componentId = routeComponent.removePrefix("<").takeWhile { it !in setOf('>', ' ') }
+        val dependencyResolver by lazy { if (JavalinVue.isDev == true) VueDependencyResolver(paths) else JavalinVue.cachedDependencyResolver }
+        val view = JavalinVue.createLayout(paths, if (JavalinVue.optimizeDependencies) dependencyResolver.resolve(componentId) else getAllDependencies(paths))
+        if (!view.contains(componentId)) {
             ctx.result("Route component not found: $routeComponent")
             return
         }
@@ -112,7 +119,7 @@ object FileInliner {
     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
+        val pathMap = paths.filterNot { it.isVueFile() } // 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
@@ -128,6 +135,7 @@ object FileInliner {
 }
 
 fun Path.readText() = String(Files.readAllBytes(this))
+fun Path.isVueFile() = this.toString().endsWith(".vue")
 
 fun escape(string: String?) = string?.toCharArray()?.map {
     when (it) {
diff --git a/javalin/src/main/java/io/javalin/plugin/rendering/vue/VueDependencyResolver.java b/javalin/src/main/java/io/javalin/plugin/rendering/vue/VueDependencyResolver.java
new file mode 100644
index 00000000..638d0593
--- /dev/null
+++ b/javalin/src/main/java/io/javalin/plugin/rendering/vue/VueDependencyResolver.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2020 tareq.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.javalin.plugin.rendering.vue;
+
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * @author tareq
+ */
+public class VueDependencyResolver {
+
+    private final Map<String, String> componentIdToOwnContent; // {component-id: component-content}
+    private final Map<String, String> componentIdToDependencyContent; // {component-id: required-dependencies}
+    private final Pattern tagRegex = Pattern.compile("<\\s*([a-z|-]*)\\s*.*>");
+    private final Pattern componentRegex = Pattern.compile("Vue.component\\(\\s*[\"|'](.*)[\"|']\\s*,.*");
+
+    public VueDependencyResolver(final Set<Path> paths) {
+        componentIdToOwnContent = new HashMap<>();
+        componentIdToDependencyContent = new HashMap<>();
+        paths.stream().filter(JavalinVueKt::isVueFile).forEach(path -> {
+            String fileContent = JavalinVueKt.readText(path);
+            Matcher matcher = componentRegex.matcher(fileContent); // check for a vue component
+            while (matcher.find()) {
+                componentIdToOwnContent.put(matcher.group(1), fileContent); // cache the file content, bound to the component name
+            }
+        });
+    }
+
+    /**
+     * Build the HTML of components needed for this component
+     *
+     * @param componentId the component-id to build the HTMl for.
+     * @return a HTML string of the components needed for this page/view if the
+     * component is found, an error string otherwise.
+     */
+    public String resolve(final String componentId) {
+        if (!componentIdToOwnContent.containsKey(componentId)) {
+            throw new IllegalArgumentException(String.format("Component %s not found", componentId));
+        }
+        if (componentIdToDependencyContent.containsKey(componentId)) {
+            return componentIdToDependencyContent.get(componentId);
+        }
+        Set<String> dependencies = resolveTransitiveDependencies(componentId);
+
+        StringBuilder builder = new StringBuilder();
+        dependencies.forEach(dependency -> {
+            builder.append("<!-- ").append(dependency).append("-->\n");
+            builder.append(componentIdToOwnContent.get(dependency));
+            builder.append("\n");
+        });
+        String allDependencies = builder.toString();
+        componentIdToDependencyContent.put(componentId, allDependencies);
+        return allDependencies;
+    }
+
+    /**
+     * Resolve the dependencies for a required component based on the contents
+     * of its file
+     *
+     * @param componentId the name of the component, without tags
+     * @return a Set of dependencies needed to render this component
+     */
+    private Set<String> resolveTransitiveDependencies(final String componentId) {
+        Set<String> requiredComponents = new HashSet<>();
+        requiredComponents.add(componentId);// add it to the dependency list
+        Set<String> directDependencies = resolveDirectDependencies(componentId); //get its dependencies
+        requiredComponents.addAll(directDependencies); //add all its dependencies  to the required components list
+        directDependencies.forEach(dependency -> {
+            // resolve each dependency
+            requiredComponents.addAll(resolveTransitiveDependencies(dependency));
+        });
+        return requiredComponents;
+    }
+
+    /**
+     * Resolve the direct dependencies for a component
+     *
+     * @param componentId the component to resolve dependencies for.
+     * @return a set of dependencies.
+     */
+    private Set<String> resolveDirectDependencies(final String componentId) {
+        Set<String> dependencies = new HashSet<>();
+        String componentContent = componentIdToOwnContent.get(componentId);
+        Matcher matcher = tagRegex.matcher(componentContent); //match for HTML tags
+        while (matcher.find()) {
+            String match = matcher.group(1);
+            if (!match.equals(componentId) && componentIdToOwnContent.containsKey(match)) { // if it isn't the component itself, and its in the component map
+                dependencies.add(match); //add it to the list of dependencies
+            }
+        }
+        return dependencies;
+    }
+
+}


Test patch:
diff --git a/javalin/src/test/java/io/javalin/TestJavalinVue.kt b/javalin/src/test/java/io/javalin/TestJavalinVue.kt
index e309f290..8fb589df 100644
--- a/javalin/src/test/java/io/javalin/TestJavalinVue.kt
+++ b/javalin/src/test/java/io/javalin/TestJavalinVue.kt
@@ -22,11 +22,13 @@ import java.nio.file.Paths
 
 class TestJavalinVue {
 
+
     @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
+        JavalinVue.optimizeDependencies = false
     }
 
     data class User(val name: String, val email: String)
@@ -129,6 +131,7 @@ class TestJavalinVue {
 
     @Test
     fun `non-existent folder fails`() = TestUtil.test { app, http ->
+        JavalinVue.isDev = true // reset
         JavalinVue.rootDirectory("/vue", Location.EXTERNAL)
         app.get("/fail", VueComponent("test-component"))
         assertThat(http.get("/fail").status).isEqualTo(500)
@@ -137,6 +140,7 @@ class TestJavalinVue {
     @Test
     fun `@cdnWebjar resolves to webjar on localhost`() = TestUtil.test { app, http ->
         val ctx = mockk<Context>(relaxed = true)
+        JavalinVue.isDev = true // reset
         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)) } }
diff --git a/javalin/src/test/java/io/javalin/TestJavalinVueResolution.java b/javalin/src/test/java/io/javalin/TestJavalinVueResolution.java
new file mode 100644
index 00000000..847b9342
--- /dev/null
+++ b/javalin/src/test/java/io/javalin/TestJavalinVueResolution.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2020 tareq.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.javalin;
+
+import io.javalin.http.staticfiles.Location;
+import io.javalin.plugin.rendering.vue.JavalinVue;
+import io.javalin.plugin.rendering.vue.VueComponent;
+import io.javalin.testing.TestUtil;
+import org.junit.Before;
+import org.junit.Test;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author tareq
+ */
+public class TestJavalinVueResolution {
+
+    @Before
+    public void resetJavalinVue() {
+        JavalinVue.rootDirectory("src/test/resources/vue", Location.EXTERNAL); // src/main -> src/test
+        JavalinVue.optimizeDependencies = true;
+    }
+
+    @Test
+    public void resoleAllDependenciesTest() {
+        TestUtil.test((server, httpUtil) -> {
+            JavalinVue.optimizeDependencies = false;
+            server.get("/non-optimized", new VueComponent("<test-component></test-component>"));
+            String body = httpUtil.getBody("/non-optimized");
+            assertThat(body).contains("<body><test-component></test-component></body>");
+            assertThat(body).contains("view-one");
+            assertThat(body).contains("view-two");
+            assertThat(body).contains("view-three");
+            assertThat(body).contains("dependency-one");
+            assertThat(body).contains("dependency-two");
+            assertThat(body).contains("dependency-three");
+            assertThat(body).contains("dependency-four");
+            assertThat(body).contains("nested-dependency");
+            assertThat(body).contains("view-nested-dependency");
+            assertThat(body).contains("multi-dependency");
+        });
+    }
+
+    @Test
+    public void resolveSingleDependencyTest() {
+        TestUtil.test((server, httpUtil) -> {
+            server.get("/single-view", new VueComponent("<view-one></view-one>"));
+            String body = httpUtil.getBody("/single-view");
+            assertThat(body).contains("<body><view-one></view-one></body>");
+            assertThat(body).doesNotContain("<view-two>");
+            assertThat(body).doesNotContain("<view-three>");
+            assertThat(body).doesNotContain("<view-nested-dependency>");
+            assertThat(body).contains("dependency-one");
+            assertThat(body).doesNotContain("dependency-two");
+            assertThat(body).doesNotContain("dependency-three");
+            assertThat(body).doesNotContain("dependency-four");
+            assertThat(body).doesNotContain("nested-dependency");
+        });
+    }
+
+    @Test
+    public void resolveNestedDependencyTest() {
+        TestUtil.test((server, httpUtil) -> {
+            server.get("/nested-view", new VueComponent("<view-nested-dependency></view-nested-dependency>"));
+            String body = httpUtil.getBody("/nested-view");
+            assertThat(body).doesNotContain("<view-one>");
+            assertThat(body).doesNotContain("<view-two>");
+            assertThat(body).doesNotContain("<view-three>");
+            assertThat(body).contains("<body><view-nested-dependency></view-nested-dependency></body>");
+            assertThat(body).contains("dependency-one");
+            assertThat(body).contains("dependency-two");
+            assertThat(body).doesNotContain("dependency-three");
+            assertThat(body).doesNotContain("dependency-four");
+            assertThat(body).contains("nested-dependency");
+        });
+    }
+
+    @Test
+    public void resolveMultiComponentFileDependencyTest() {
+        TestUtil.test((server, httpUtil) -> {
+            server.get("/multi-view-one", new VueComponent("<view-two></view-two>"));
+            String body = httpUtil.getBody("/multi-view-one");
+            assertThat(body).doesNotContain("<view-one>");
+            assertThat(body).contains("<body><view-two></view-two></body>");
+            assertThat(body).doesNotContain("<view-three>");
+            assertThat(body).doesNotContain("<view-nested-dependency>");
+            assertThat(body).doesNotContain("dependency-one");
+            assertThat(body).doesNotContain("dependency-two");
+            assertThat(body).contains("dependency-three");
+            assertThat(body).contains("dependency-four");
+            assertThat(body).doesNotContain("nested-dependency");
+
+            server.get("/multi-view-two", new VueComponent("<view-three></view-three>"));
+            body = httpUtil.getBody("/multi-view-two");
+            assertThat(body).doesNotContain("<view-one>");
+            assertThat(body).doesNotContain("<view-two>");
+            assertThat(body).contains("<body><view-three></view-three></body>");
+            assertThat(body).doesNotContain("<view-nested-dependency>");
+            assertThat(body).doesNotContain("dependency-one");
+            assertThat(body).doesNotContain("dependency-two");
+            assertThat(body).contains("dependency-three");
+            assertThat(body).contains("dependency-four");
+            assertThat(body).doesNotContain("nested-dependency");
+        });
+    }
+
+}
diff --git a/javalin/src/test/resources/vue/dependency-one.vue b/javalin/src/test/resources/vue/dependency-one.vue
new file mode 100644
index 00000000..bba98f57
--- /dev/null
+++ b/javalin/src/test/resources/vue/dependency-one.vue
@@ -0,0 +1,6 @@
+<template id="dependency-one">
+    <div>Dependency One</div>
+</template>
+<script>
+    Vue.component('dependency-one',{template:"#dependency-one"});
+</script>
diff --git a/javalin/src/test/resources/vue/dependency-two.vue b/javalin/src/test/resources/vue/dependency-two.vue
new file mode 100644
index 00000000..957ae308
--- /dev/null
+++ b/javalin/src/test/resources/vue/dependency-two.vue
@@ -0,0 +1,7 @@
+<template id="dependency-two">
+    <div>Dependency Two</div>
+</template>
+<script>
+    Vue.component('dependency-two',{template:"#dependency-two"});
+</script>
+
diff --git a/javalin/src/test/resources/vue/layout.html b/javalin/src/test/resources/vue/layout.html
index a4364762..90fcff09 100644
--- a/javalin/src/test/resources/vue/layout.html
+++ b/javalin/src/test/resources/vue/layout.html
@@ -6,3 +6,4 @@
     <script src="@cdnWebjar/swagger-ui/3.24.3/swagger-ui.css"></script>
 </head>
 <body>@routeComponent</body>
+
diff --git a/javalin/src/test/resources/vue/multi-dependency.vue b/javalin/src/test/resources/vue/multi-dependency.vue
new file mode 100644
index 00000000..bb6c40b2
--- /dev/null
+++ b/javalin/src/test/resources/vue/multi-dependency.vue
@@ -0,0 +1,11 @@
+<template id="dependency-three">
+    <div>Dependency Three</div>
+</template>
+<template id="dependency-four">
+    <div>Dependency Four</div>
+</template>
+<script>
+    Vue.component('dependency-three',{template:"#dependency-three"});
+    Vue.component('dependency-four',{template:"#dependency-four"});
+</script>
+
diff --git a/javalin/src/test/resources/vue/nested-dependency.vue b/javalin/src/test/resources/vue/nested-dependency.vue
new file mode 100644
index 00000000..f8ca6d67
--- /dev/null
+++ b/javalin/src/test/resources/vue/nested-dependency.vue
@@ -0,0 +1,8 @@
+<template id="nested-dependency">
+    <dependency-one></dependency-one>
+    <dependency-two></dependency-two>
+</template>
+<script>
+    Vue.component('nested-dependency',{template:"#nested-dependency"})
+</script>
+
diff --git a/javalin/src/test/resources/vue/test-component.vue b/javalin/src/test/resources/vue/test-component.vue
index b3843d38..97fa0363 100644
--- a/javalin/src/test/resources/vue/test-component.vue
+++ b/javalin/src/test/resources/vue/test-component.vue
@@ -4,3 +4,4 @@
 <script>
     Vue.component("test-component", {template: "#test-component"});
 </script>
+
diff --git a/javalin/src/test/resources/vue/view-nested-dependency.vue b/javalin/src/test/resources/vue/view-nested-dependency.vue
new file mode 100644
index 00000000..55fdd845
--- /dev/null
+++ b/javalin/src/test/resources/vue/view-nested-dependency.vue
@@ -0,0 +1,6 @@
+<template id="view-nested-dependency">
+    <nested-dependency></nested-dependency>
+</template>
+<script>
+    Vue.component("view-nested-dependency",{template:"#view-nested-dependency"});
+</script>
diff --git a/javalin/src/test/resources/vue/view-one.vue b/javalin/src/test/resources/vue/view-one.vue
new file mode 100644
index 00000000..8bd42651
--- /dev/null
+++ b/javalin/src/test/resources/vue/view-one.vue
@@ -0,0 +1,7 @@
+<template id="view-one">
+    <dependency-one></dependency-one>
+</template>
+<script>
+    Vue.component("view-one",{template:"#view-one"})
+</script>
+
diff --git a/javalin/src/test/resources/vue/view-three.vue b/javalin/src/test/resources/vue/view-three.vue
new file mode 100644
index 00000000..f3e96e13
--- /dev/null
+++ b/javalin/src/test/resources/vue/view-three.vue
@@ -0,0 +1,7 @@
+<template id="view-three">
+    <dependency-four></dependency-four>
+</template>
+<script>
+    Vue.component("view-three",{template:"#view-three"})
+</script>
+
diff --git a/javalin/src/test/resources/vue/view-two.vue b/javalin/src/test/resources/vue/view-two.vue
new file mode 100644
index 00000000..77f07a81
--- /dev/null
+++ b/javalin/src/test/resources/vue/view-two.vue
@@ -0,0 +1,7 @@
+<template id="view-two">
+    <dependency-three></dependency-three>
+</template>
+<script>
+    Vue.component("view-two",{template:"#view-two"})
+</script>
+