diff --git a/packages/java/endpoint/src/main/java/com/vaadin/hilla/route/ClientRouteRegistry.java b/packages/java/endpoint/src/main/java/com/vaadin/hilla/route/ClientRouteRegistry.java index 39cbd5f425..43db427d91 100644 --- a/packages/java/endpoint/src/main/java/com/vaadin/hilla/route/ClientRouteRegistry.java +++ b/packages/java/endpoint/src/main/java/com/vaadin/hilla/route/ClientRouteRegistry.java @@ -15,11 +15,19 @@ */ package com.vaadin.hilla.route; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.vaadin.flow.function.DeploymentConfiguration; import com.vaadin.hilla.route.records.ClientViewConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import org.springframework.util.AntPathMatcher; +import java.io.IOException; import java.io.Serializable; +import java.net.MalformedURLException; +import java.net.URL; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; @@ -36,6 +44,11 @@ public class ClientRouteRegistry implements Serializable { */ private final Map registeredRoutes = new LinkedHashMap<>(); + private final ObjectMapper mapper = new ObjectMapper(); + + private static final Logger LOGGER = LoggerFactory + .getLogger(ClientRouteRegistry.class); + /** * Returns all registered routes. * @@ -91,4 +104,56 @@ public ClientViewConfig getRouteByPath(String path) { } return null; } + + /** + * Registers client routes from views.json file generated by the + * file-router's Vite plugin. The views.json file is expected to be in the + * frontend/generated folder in dev mode and in the META-INF/VAADIN folder + * in production mode. + * + * @param deploymentConfiguration + * the deployment configuration + */ + public void registerClientRoutes( + DeploymentConfiguration deploymentConfiguration) { + try (var source = getViewsJsonAsResource(deploymentConfiguration) + .openStream()) { + if (source != null) { + clearRoutes(); + registerAndRecurseChildren("", + mapper.readValue(source, new TypeReference<>() { + })); + } else { + LOGGER.warn("Failed to find views.json"); + } + } catch (IOException e) { + LOGGER.warn("Failed extract client views from views.json", e); + } + } + + private URL getViewsJsonAsResource( + DeploymentConfiguration deploymentConfiguration) + throws MalformedURLException { + var isProductionMode = deploymentConfiguration.isProductionMode(); + if (isProductionMode) { + return getClass().getResource("/META-INF/VAADIN/views.json"); + } + return deploymentConfiguration.getFrontendFolder().toPath() + .resolve("generated").resolve("views.json").toUri().toURL(); + } + + private void registerAndRecurseChildren(String basePath, + ClientViewConfig view) { + var path = view.getRoute() == null || view.getRoute().isEmpty() + ? basePath + : basePath + '/' + view.getRoute(); + if (view.getChildren() == null || view.getChildren().isEmpty()) { + addRoute(path, view); + } else { + view.getChildren().forEach(child -> { + child.setParent(view); + registerAndRecurseChildren(path, child); + }); + } + } } diff --git a/packages/java/endpoint/src/main/java/com/vaadin/hilla/route/RouteExtractionIndexHtmlRequestListener.java b/packages/java/endpoint/src/main/java/com/vaadin/hilla/route/RouteUnifyingIndexHtmlRequestListener.java similarity index 74% rename from packages/java/endpoint/src/main/java/com/vaadin/hilla/route/RouteExtractionIndexHtmlRequestListener.java rename to packages/java/endpoint/src/main/java/com/vaadin/hilla/route/RouteUnifyingIndexHtmlRequestListener.java index 3a3293c56b..01c6a2406c 100644 --- a/packages/java/endpoint/src/main/java/com/vaadin/hilla/route/RouteExtractionIndexHtmlRequestListener.java +++ b/packages/java/endpoint/src/main/java/com/vaadin/hilla/route/RouteUnifyingIndexHtmlRequestListener.java @@ -16,14 +16,16 @@ package com.vaadin.hilla.route; import java.io.IOException; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; import java.util.HashMap; import java.util.Map; +import com.vaadin.flow.function.DeploymentConfiguration; import org.jsoup.nodes.DataNode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; import com.fasterxml.jackson.databind.ObjectMapper; @@ -40,28 +42,33 @@ * Index HTML request listener for collecting the client side and the server * side views and adding them to index.html response. */ -@Component -public class RouteExtractionIndexHtmlRequestListener +public class RouteUnifyingIndexHtmlRequestListener implements IndexHtmlRequestListener { protected static final String SCRIPT_STRING = """ window.Vaadin = window.Vaadin ?? {}; window.Vaadin.server = window.Vaadin.server ?? {}; window.Vaadin.server.views = %s;"""; private static final Logger LOGGER = LoggerFactory - .getLogger(RouteExtractionIndexHtmlRequestListener.class); + .getLogger(RouteUnifyingIndexHtmlRequestListener.class); private final ClientRouteRegistry clientRouteRegistry; private final ObjectMapper mapper = new ObjectMapper(); + private final DeploymentConfiguration deploymentConfiguration; + + private LocalDateTime lastUpdated; /** * Creates a new listener instance with the given route registry. * * @param clientRouteRegistry * the client route registry for getting the client side views + * @param deploymentConfiguration + * the runtime deployment configuration */ - @Autowired - public RouteExtractionIndexHtmlRequestListener( - ClientRouteRegistry clientRouteRegistry) { + public RouteUnifyingIndexHtmlRequestListener( + ClientRouteRegistry clientRouteRegistry, + DeploymentConfiguration deploymentConfiguration) { this.clientRouteRegistry = clientRouteRegistry; + this.deploymentConfiguration = deploymentConfiguration; } @Override @@ -85,6 +92,12 @@ public void modifyIndexHtmlResponse(IndexHtmlResponse response) { protected void collectClientViews( Map availableViews) { + if (!deploymentConfiguration.isProductionMode()) { + loadLatestDevModeViewsJsonIfNeeded(); + } else if (lastUpdated == null) { + // initial (and only) registration in production mode: + registerClientRoutes(LocalDateTime.now()); + } clientRouteRegistry.getAllRoutes().forEach((route, config) -> { final AvailableViewInfo availableViewInfo = new AvailableViewInfo( config.getTitle(), config.getRolesAllowed(), @@ -96,6 +109,28 @@ protected void collectClientViews( } + private void loadLatestDevModeViewsJsonIfNeeded() { + var devModeViewsJsonFile = deploymentConfiguration.getFrontendFolder() + .toPath().resolve("generated").resolve("views.json").toFile(); + if (!devModeViewsJsonFile.exists()) { + LOGGER.warn("Failed to find views.json under {}", + deploymentConfiguration.getFrontendFolder().toPath() + .resolve("generated")); + return; + } + var lastModified = devModeViewsJsonFile.lastModified(); + var lastModifiedTime = Instant.ofEpochMilli(lastModified) + .atZone(ZoneId.systemDefault()).toLocalDateTime(); + if (lastUpdated == null || lastModifiedTime.isAfter(lastUpdated)) { + registerClientRoutes(lastModifiedTime); + } + } + + private void registerClientRoutes(LocalDateTime newLastUpdated) { + lastUpdated = newLastUpdated; + clientRouteRegistry.registerClientRoutes(deploymentConfiguration); + } + protected void collectServerViews( final Map serverViews) { final VaadinService vaadinService = VaadinService.getCurrent(); diff --git a/packages/java/endpoint/src/main/java/com/vaadin/hilla/startup/RouteUnifyingServiceInitListener.java b/packages/java/endpoint/src/main/java/com/vaadin/hilla/startup/RouteUnifyingServiceInitListener.java index 33fdf39568..de7b35aa0b 100644 --- a/packages/java/endpoint/src/main/java/com/vaadin/hilla/startup/RouteUnifyingServiceInitListener.java +++ b/packages/java/endpoint/src/main/java/com/vaadin/hilla/startup/RouteUnifyingServiceInitListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2022 Vaadin Ltd. + * Copyright 2000-2024 Vaadin Ltd. * * 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 @@ -16,24 +16,19 @@ package com.vaadin.hilla.startup; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; import com.vaadin.flow.server.ServiceInitEvent; import com.vaadin.flow.server.VaadinServiceInitListener; import com.vaadin.hilla.route.ClientRouteRegistry; -import com.vaadin.hilla.route.RouteExtractionIndexHtmlRequestListener; -import com.vaadin.hilla.route.records.ClientViewConfig; +import com.vaadin.hilla.route.RouteUnifyingIndexHtmlRequestListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import java.io.IOException; - /** * Service init listener to add the - * {@link RouteExtractionIndexHtmlRequestListener} to the service and to - * register client routes to {@link ClientRouteRegistry}. + * {@link RouteUnifyingIndexHtmlRequestListener} to the service and to register + * client routes to {@link ClientRouteRegistry}. */ @Component public class RouteUnifyingServiceInitListener @@ -41,61 +36,33 @@ public class RouteUnifyingServiceInitListener private static final Logger LOGGER = LoggerFactory .getLogger(RouteUnifyingServiceInitListener.class); - private final RouteExtractionIndexHtmlRequestListener routeExtractionIndexHtmlRequestListener; private final ClientRouteRegistry clientRouteRegistry; - private final ObjectMapper mapper = new ObjectMapper(); /** * Creates a new instance of the listener. * - * @param routeExtractionIndexHtmlRequestListener - * the listener to add * @param clientRouteRegistry * the registry to add the client routes to */ @Autowired public RouteUnifyingServiceInitListener( - RouteExtractionIndexHtmlRequestListener routeExtractionIndexHtmlRequestListener, ClientRouteRegistry clientRouteRegistry) { - this.routeExtractionIndexHtmlRequestListener = routeExtractionIndexHtmlRequestListener; this.clientRouteRegistry = clientRouteRegistry; } @Override public void serviceInit(ServiceInitEvent event) { - registerClientRoutes(); + var deploymentConfiguration = event.getSource() + .getDeploymentConfiguration(); + var routeExtractionIndexHtmlRequestListener = new RouteUnifyingIndexHtmlRequestListener( + clientRouteRegistry, deploymentConfiguration); + var deploymentMode = deploymentConfiguration.isProductionMode() + ? "PRODUCTION" + : "DEVELOPMENT"; event.addIndexHtmlRequestListener( routeExtractionIndexHtmlRequestListener); - } - - protected void registerClientRoutes() { - try (var source = getClass() - .getResourceAsStream("/META-INF/VAADIN/views.json")) { - if (source != null) { - clientRouteRegistry.clearRoutes(); - registerAndRecurseChildren("", - mapper.readValue(source, new TypeReference<>() { - })); - } else { - LOGGER.warn("Failed to find views.json"); - } - } catch (IOException e) { - LOGGER.warn("Failed extract client views from views.json", e); - } - } - - private void registerAndRecurseChildren(String basePath, - ClientViewConfig view) { - var path = view.getRoute() == null || view.getRoute().isEmpty() - ? basePath - : basePath + '/' + view.getRoute(); - if (view.getChildren() == null || view.getChildren().isEmpty()) { - clientRouteRegistry.addRoute(path, view); - } else { - view.getChildren().forEach(child -> { - child.setParent(view); - registerAndRecurseChildren(path, child); - }); - } + LOGGER.debug( + "{} mode: Registered RouteUnifyingIndexHtmlRequestListener.", + deploymentMode); } } diff --git a/packages/java/endpoint/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/packages/java/endpoint/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index de1fa068ab..d9d93fdc48 100644 --- a/packages/java/endpoint/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/packages/java/endpoint/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -5,6 +5,5 @@ com.vaadin.hilla.internal.hotswap.HotSwapConfiguration com.vaadin.hilla.crud.CrudConfiguration com.vaadin.hilla.startup.EndpointRegistryInitializer com.vaadin.hilla.startup.RouteUnifyingServiceInitListener -com.vaadin.hilla.route.RouteExtractionIndexHtmlRequestListener com.vaadin.hilla.route.ClientRouteRegistry com.vaadin.hilla.route.RouteUtil diff --git a/packages/java/endpoint/src/test/java/com/vaadin/hilla/route/ClientRouteRegistryTest.java b/packages/java/endpoint/src/test/java/com/vaadin/hilla/route/ClientRouteRegistryTest.java new file mode 100644 index 0000000000..16a6dbccfa --- /dev/null +++ b/packages/java/endpoint/src/test/java/com/vaadin/hilla/route/ClientRouteRegistryTest.java @@ -0,0 +1,162 @@ +package com.vaadin.hilla.route; + +import com.vaadin.flow.function.DeploymentConfiguration; +import com.vaadin.hilla.route.records.ClientViewConfig; +import com.vaadin.hilla.route.records.RouteParamType; +import org.apache.commons.io.IOUtils; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.mockito.Mockito; + +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +public class ClientRouteRegistryTest { + + private final ClientRouteRegistry clientRouteRegistry = new ClientRouteRegistry(); + + private final DeploymentConfiguration deploymentConfiguration = Mockito + .mock(DeploymentConfiguration.class); + + @Rule + public TemporaryFolder projectRoot = new TemporaryFolder(); + + @Test + public void when_clearRoutes_isCalled_then_allRoutesAreCleared() + throws IOException { + mockDevelopmentMode(); + createMockedDevModeViewsJson(); + + clientRouteRegistry.registerClientRoutes(deploymentConfiguration); + Map allRoutes = clientRouteRegistry + .getAllRoutes(); + MatcherAssert.assertThat(allRoutes, Matchers.aMapWithSize(12)); + + clientRouteRegistry.clearRoutes(); + MatcherAssert.assertThat(clientRouteRegistry.getAllRoutes(), + Matchers.anEmptyMap()); + } + + @Test + public void when_developmentMode_and_noViewsJsonFile_then_noRoutesAreRegistered() + throws IOException { + + mockDevelopmentMode(); + + clientRouteRegistry.registerClientRoutes(deploymentConfiguration); + Map allRoutes = clientRouteRegistry + .getAllRoutes(); + MatcherAssert.assertThat(allRoutes, Matchers.anEmptyMap()); + } + + @Test + public void when_developmentMode_and_emptyViewsJsonFile_then_noRoutesAreRegistered() + throws IOException { + + mockDevelopmentMode(); + + projectRoot.newFile("frontend/generated/views.json"); + + clientRouteRegistry.registerClientRoutes(deploymentConfiguration); + Map allRoutes = clientRouteRegistry + .getAllRoutes(); + MatcherAssert.assertThat(allRoutes, Matchers.anEmptyMap()); + } + + @Test + public void when_productionMode_then_loadClientViewsFromResources() { + + Mockito.when(deploymentConfiguration.isProductionMode()) + .thenReturn(true); + + clientRouteRegistry.registerClientRoutes(deploymentConfiguration); + Map allRoutes = clientRouteRegistry + .getAllRoutes(); + + MatcherAssert.assertThat(allRoutes, Matchers.aMapWithSize(12)); + MatcherAssert.assertThat( + clientRouteRegistry.getRouteByPath("/about").getTitle(), + Matchers.is("About")); + MatcherAssert.assertThat( + clientRouteRegistry.getRouteByPath("/profile/friends/list") + .getOther().get("unknown"), + Matchers.notNullValue()); + MatcherAssert.assertThat( + clientRouteRegistry + .getRouteByPath("/profile/friends/:user?/edit") + .getRouteParameters(), + Matchers.is(Map.of(":user?", RouteParamType.OPTIONAL))); + MatcherAssert.assertThat( + clientRouteRegistry.getRouteByPath("/profile/friends/:user") + .getRouteParameters(), + Matchers.is(Map.of(":user", RouteParamType.REQUIRED))); + MatcherAssert.assertThat( + clientRouteRegistry.getRouteByPath("/profile/messages/*") + .getRouteParameters(), + Matchers.is(Map.of("wildcard", RouteParamType.WILDCARD))); + } + + @Test + public void when_developmentMode_then_loadClientViewsFromFrontendGenerated() + throws IOException { + + mockDevelopmentMode(); + createMockedDevModeViewsJson(); + + clientRouteRegistry.registerClientRoutes(deploymentConfiguration); + Map allRoutes = clientRouteRegistry + .getAllRoutes(); + + MatcherAssert.assertThat(allRoutes, Matchers.aMapWithSize(12)); + MatcherAssert.assertThat( + clientRouteRegistry.getRouteByPath("/dev/about").getTitle(), + Matchers.is("About")); + MatcherAssert.assertThat( + clientRouteRegistry.getRouteByPath("/dev/profile/friends/list") + .getOther().get("unknown"), + Matchers.notNullValue()); + MatcherAssert + .assertThat( + clientRouteRegistry + .getRouteByPath( + "/dev/profile/friends/:user?/edit") + .getRouteParameters(), + Matchers.is(Map.of(":user?", RouteParamType.OPTIONAL))); + MatcherAssert.assertThat( + clientRouteRegistry.getRouteByPath("/dev/profile/friends/:user") + .getRouteParameters(), + Matchers.is(Map.of(":user", RouteParamType.REQUIRED))); + MatcherAssert.assertThat( + clientRouteRegistry.getRouteByPath("/dev/profile/messages/*") + .getRouteParameters(), + Matchers.is(Map.of("wildcard", RouteParamType.WILDCARD))); + } + + private void mockDevelopmentMode() throws IOException { + Mockito.when(deploymentConfiguration.isProductionMode()) + .thenReturn(false); + var frontendGeneratedDir = projectRoot.newFolder("frontend/generated"); + Mockito.when(deploymentConfiguration.getFrontendFolder()) + .thenReturn(frontendGeneratedDir.getParentFile()); + } + + private void createMockedDevModeViewsJson() throws IOException { + var viewsJsonProdAsResource = getClass() + .getResource("/META-INF/VAADIN/views.json"); + assert viewsJsonProdAsResource != null; + String hierarchicalRoutesAsString = IOUtils.toString( + viewsJsonProdAsResource.openStream(), StandardCharsets.UTF_8); + String addedDevToRootRoute = hierarchicalRoutesAsString + .replaceFirst("\"route\": \"\",", "\"route\": \"dev\","); + var viewsJsonFile = projectRoot + .newFile("frontend/generated/views.json"); + try (PrintWriter writer = new PrintWriter(viewsJsonFile)) { + writer.println(addedDevToRootRoute); + } + } +} diff --git a/packages/java/endpoint/src/test/java/com/vaadin/hilla/route/RouteExtractionIndexHtmlRequestListenerTest.java b/packages/java/endpoint/src/test/java/com/vaadin/hilla/route/RouteUnifyingIndexHtmlRequestListenerTest.java similarity index 73% rename from packages/java/endpoint/src/test/java/com/vaadin/hilla/route/RouteExtractionIndexHtmlRequestListenerTest.java rename to packages/java/endpoint/src/test/java/com/vaadin/hilla/route/RouteUnifyingIndexHtmlRequestListenerTest.java index fc9f7d00d5..216900b6b5 100644 --- a/packages/java/endpoint/src/test/java/com/vaadin/hilla/route/RouteExtractionIndexHtmlRequestListenerTest.java +++ b/packages/java/endpoint/src/test/java/com/vaadin/hilla/route/RouteUnifyingIndexHtmlRequestListenerTest.java @@ -7,17 +7,19 @@ import java.util.List; import java.util.Map; +import com.vaadin.flow.function.DeploymentConfiguration; import org.hamcrest.MatcherAssert; import org.hamcrest.Matchers; import org.jsoup.nodes.DataNode; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; import org.mockito.MockedStatic; import org.mockito.Mockito; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.vaadin.flow.component.Component; @@ -32,22 +34,30 @@ import com.vaadin.hilla.route.records.ClientViewConfig; import com.vaadin.hilla.route.records.RouteParamType; -public class RouteExtractionIndexHtmlRequestListenerTest { +public class RouteUnifyingIndexHtmlRequestListenerTest { - protected static final String SCRIPT_STRING = RouteExtractionIndexHtmlRequestListener.SCRIPT_STRING + protected static final String SCRIPT_STRING = RouteUnifyingIndexHtmlRequestListener.SCRIPT_STRING .replace("%s;", ""); private final ClientRouteRegistry clientRouteRegistry = Mockito .mock(ClientRouteRegistry.class); - private final RouteExtractionIndexHtmlRequestListener requestListener = new RouteExtractionIndexHtmlRequestListener( - clientRouteRegistry); + private RouteUnifyingIndexHtmlRequestListener requestListener; private IndexHtmlResponse indexHtmlResponse; - private VaadinService vaadinService; + private DeploymentConfiguration deploymentConfiguration; + + @Rule + public TemporaryFolder projectRoot = new TemporaryFolder(); @Before public void setUp() { vaadinService = Mockito.mock(VaadinService.class); + deploymentConfiguration = Mockito.mock(DeploymentConfiguration.class); + Mockito.when(vaadinService.getDeploymentConfiguration()) + .thenReturn(deploymentConfiguration); + requestListener = new RouteUnifyingIndexHtmlRequestListener( + clientRouteRegistry, deploymentConfiguration); + indexHtmlResponse = Mockito.mock(IndexHtmlResponse.class); final Document document = Mockito.mock(Document.class); @@ -148,12 +158,13 @@ private static List prepareServerRoutes() { } @Test - public void should_modifyIndexHtmlResponse() - throws JsonProcessingException, IOException { + public void when_productionMode_should_modifyIndexHtmlResponse() + throws IOException { try (MockedStatic mocked = Mockito .mockStatic(VaadinService.class)) { mocked.when(VaadinService::getCurrent).thenReturn(vaadinService); - + Mockito.when(deploymentConfiguration.isProductionMode()) + .thenReturn(true); requestListener.modifyIndexHtmlResponse(indexHtmlResponse); } Mockito.verify(indexHtmlResponse, Mockito.times(1)).getDocument(); @@ -177,7 +188,38 @@ public void should_modifyIndexHtmlResponse() .getResource("/META-INF/VAADIN/available-views.json")); MatcherAssert.assertThat(actual, Matchers.is(expected)); + } + + @Test + public void when_developmentMode_should_modifyIndexHtmlResponse() + throws IOException { + try (MockedStatic mocked = Mockito + .mockStatic(VaadinService.class)) { + mocked.when(VaadinService::getCurrent).thenReturn(vaadinService); + mockDevelopmentMode(); + requestListener.modifyIndexHtmlResponse(indexHtmlResponse); + } + Mockito.verify(indexHtmlResponse, Mockito.times(1)).getDocument(); + MatcherAssert.assertThat( + indexHtmlResponse.getDocument().head().select("script"), + Matchers.notNullValue()); + + DataNode script = indexHtmlResponse.getDocument().head() + .select("script").dataNodes().get(0); + + final String scriptText = script.getWholeData(); + MatcherAssert.assertThat(scriptText, + Matchers.startsWith(SCRIPT_STRING)); + + final String views = scriptText.substring(SCRIPT_STRING.length()); + + final var mapper = new ObjectMapper(); + + var actual = mapper.readTree(views); + var expected = mapper.readTree(getClass() + .getResource("/META-INF/VAADIN/available-views.json")); + MatcherAssert.assertThat(actual, Matchers.is(expected)); } @Test @@ -210,12 +252,31 @@ public void should_collectServerViews() { } @Test - public void should_collectClientViews() { + public void when_productionMode_should_collectClientViews() { + final Map views = new LinkedHashMap<>(); + Mockito.when(deploymentConfiguration.isProductionMode()) + .thenReturn(true); + requestListener.collectClientViews(views); + MatcherAssert.assertThat(views, Matchers.aMapWithSize(3)); + } + + @Test + public void when_developmentMode_should_collectClientViews() + throws IOException { final Map views = new LinkedHashMap<>(); + mockDevelopmentMode(); requestListener.collectClientViews(views); MatcherAssert.assertThat(views, Matchers.aMapWithSize(3)); } + private void mockDevelopmentMode() throws IOException { + Mockito.when(deploymentConfiguration.isProductionMode()) + .thenReturn(false); + var frontendGeneratedDir = projectRoot.newFolder("frontend/generated"); + Mockito.when(deploymentConfiguration.getFrontendFolder()) + .thenReturn(frontendGeneratedDir.getParentFile()); + } + @PageTitle("RouteTarget") private static class RouteTarget extends Component { } diff --git a/packages/java/endpoint/src/test/java/com/vaadin/hilla/startup/RouteUnifyingServiceInitListenerTest.java b/packages/java/endpoint/src/test/java/com/vaadin/hilla/startup/RouteUnifyingServiceInitListenerTest.java index c4e55a7992..683e7a092a 100644 --- a/packages/java/endpoint/src/test/java/com/vaadin/hilla/startup/RouteUnifyingServiceInitListenerTest.java +++ b/packages/java/endpoint/src/test/java/com/vaadin/hilla/startup/RouteUnifyingServiceInitListenerTest.java @@ -1,34 +1,43 @@ package com.vaadin.hilla.startup; -import java.util.Map; +import java.io.IOException; -import org.hamcrest.MatcherAssert; -import org.hamcrest.Matchers; import org.junit.Assert; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; import org.mockito.Mockito; +import com.vaadin.flow.function.DeploymentConfiguration; import com.vaadin.flow.server.ServiceInitEvent; import com.vaadin.flow.server.VaadinService; import com.vaadin.hilla.route.ClientRouteRegistry; -import com.vaadin.hilla.route.RouteExtractionIndexHtmlRequestListener; -import com.vaadin.hilla.route.records.ClientViewConfig; -import com.vaadin.hilla.route.records.RouteParamType; +import com.vaadin.hilla.route.RouteUnifyingIndexHtmlRequestListener; public class RouteUnifyingServiceInitListenerTest { private RouteUnifyingServiceInitListener routeUnifyingServiceInitListener; private ServiceInitEvent event; private ClientRouteRegistry clientRouteRegistry; + private DeploymentConfiguration mockDeploymentConfiguration; + + @Rule + public TemporaryFolder projectRoot = new TemporaryFolder(); @Before - public void setup() { + public void setup() throws IOException { clientRouteRegistry = new ClientRouteRegistry(); routeUnifyingServiceInitListener = new RouteUnifyingServiceInitListener( - Mockito.mock(RouteExtractionIndexHtmlRequestListener.class), clientRouteRegistry); - event = new ServiceInitEvent(Mockito.mock(VaadinService.class)); + VaadinService mockVaadinService = Mockito.mock(VaadinService.class); + mockDeploymentConfiguration = Mockito + .mock(DeploymentConfiguration.class); + Mockito.when(mockVaadinService.getDeploymentConfiguration()) + .thenReturn(mockDeploymentConfiguration); + Mockito.when(mockDeploymentConfiguration.isProductionMode()) + .thenReturn(true); + event = new ServiceInitEvent(mockVaadinService); } @Test @@ -41,33 +50,9 @@ public void should_addRouteIndexHtmlRequestListener() { eventHasAddedRouteIndexHtmlRequestListener(event)); } - @Test - public void should_extractClientViews() { - routeUnifyingServiceInitListener.registerClientRoutes(); - Map allRoutes = clientRouteRegistry - .getAllRoutes(); - - MatcherAssert.assertThat(allRoutes, Matchers.aMapWithSize(12)); - MatcherAssert.assertThat(allRoutes.get("/about").getTitle(), - Matchers.is("About")); - MatcherAssert.assertThat(allRoutes.get("/profile/friends/list") - .getOther().get("unknown"), Matchers.notNullValue()); - MatcherAssert.assertThat( - allRoutes.get("/profile/friends/:user?/edit") - .getRouteParameters(), - Matchers.is(Map.of(":user?", RouteParamType.OPTIONAL))); - MatcherAssert.assertThat( - allRoutes.get("/profile/friends/:user").getRouteParameters(), - Matchers.is(Map.of(":user", RouteParamType.REQUIRED))); - MatcherAssert.assertThat( - allRoutes.get("/profile/messages/*").getRouteParameters(), - Matchers.is(Map.of("wildcard", RouteParamType.WILDCARD))); - - } - private boolean eventHasAddedRouteIndexHtmlRequestListener( ServiceInitEvent event) { return event.getAddedIndexHtmlRequestListeners().anyMatch( - indexHtmlRequestListener -> indexHtmlRequestListener instanceof RouteExtractionIndexHtmlRequestListener); + indexHtmlRequestListener -> indexHtmlRequestListener instanceof RouteUnifyingIndexHtmlRequestListener); } } diff --git a/packages/ts/hilla-file-router/src/vite-plugin.ts b/packages/ts/hilla-file-router/src/vite-plugin.ts index 6fd5ef4441..011e91d643 100644 --- a/packages/ts/hilla-file-router/src/vite-plugin.ts +++ b/packages/ts/hilla-file-router/src/vite-plugin.ts @@ -28,6 +28,12 @@ export type PluginOptions = Readonly<{ * @defaultValue `['.tsx', '.jsx']` */ extensions?: readonly string[]; + /** + * The flag to indicate whether the plugin is running in development mode. + * + * @defaultValue `false` + */ + isDevMode?: boolean; }>; /** @@ -40,6 +46,7 @@ export default function vitePluginFileSystemRouter({ viewsDir = 'frontend/views/', generatedDir = 'frontend/generated/', extensions = ['.tsx', '.jsx'], + isDevMode = false, }: PluginOptions = {}): Plugin { const hmrInjectionPattern = /(?<=import\.meta\.hot\.accept[\s\S]+)if\s\(!nextExports\)\s+return;/u; @@ -63,7 +70,7 @@ export default function vitePluginFileSystemRouter({ _logger.info(`The output directory: ${String(_outDir)}`); runtimeUrls = { - json: new URL('views.json', _outDir), + json: new URL('views.json', isDevMode ? _generatedDir : _outDir), code: new URL('views.ts', _generatedDir), }; }, @@ -85,6 +92,7 @@ export default function vitePluginFileSystemRouter({ generateRuntimeFiles(_viewsDir, runtimeUrls, extensions, _logger).catch((e: unknown) => _logger.error(String(e)), ); + server.hot.send({ type: 'full-reload' }); }; server.watcher.on('add', changeListener);