Skip to content

Commit

Permalink
fix: generate views.json under frontend/generated in dev mode (#2234)
Browse files Browse the repository at this point in the history
* fix: generate views.json under frontend/generated in dev mode

Fixes #2213

* centralize client route registration logic in ClientRouteRegistry

* make vite-plugin send full reload signal after generating routes
  • Loading branch information
taefi authored Mar 26, 2024
1 parent ab47af1 commit 2b7ef94
Show file tree
Hide file tree
Showing 8 changed files with 383 additions and 101 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -36,6 +44,11 @@ public class ClientRouteRegistry implements Serializable {
*/
private final Map<String, ClientViewConfig> registeredRoutes = new LinkedHashMap<>();

private final ObjectMapper mapper = new ObjectMapper();

private static final Logger LOGGER = LoggerFactory
.getLogger(ClientRouteRegistry.class);

/**
* Returns all registered routes.
*
Expand Down Expand Up @@ -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);
});
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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
Expand All @@ -85,6 +92,12 @@ public void modifyIndexHtmlResponse(IndexHtmlResponse response) {

protected void collectClientViews(
Map<String, AvailableViewInfo> 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(),
Expand All @@ -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<String, AvailableViewInfo> serverViews) {
final VaadinService vaadinService = VaadinService.getCurrent();
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -16,86 +16,53 @@

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
implements VaadinServiceInitListener {
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading

0 comments on commit 2b7ef94

Please sign in to comment.