Skip to content

Commit

Permalink
Merge branch 'main' into fix/flow-router-check-client-route-conflicts
Browse files Browse the repository at this point in the history
  • Loading branch information
tepi authored Oct 16, 2024
2 parents 2a97716 + 1937249 commit dab3459
Show file tree
Hide file tree
Showing 62 changed files with 514 additions and 237 deletions.
4 changes: 2 additions & 2 deletions flow-client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion flow-client/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "@vaadin/flow-deps",
"name": "@vaadin/flow-client",
"description": "Flow client package",
"version": "0.0.1",
"main": "src/main/resources/META-INF/frontend/Flow",
Expand Down
2 changes: 1 addition & 1 deletion flow-polymer2lit/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "flow-polymer2lit",
"name": "@vaadin/flow-polymer2lit",
"version": "0.0.1",
"description": "A migration tool for Polymer to Lit for selected cases",
"author": "Vaadin Ltd",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,12 @@
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
Expand Down Expand Up @@ -73,6 +75,27 @@ public class MenuRegistry {
private static final Logger log = LoggerFactory
.getLogger(MenuRegistry.class);

/**
* File routes lazy loading and caching.
*/
private enum FileRoutesCache {
INSTANCE;

private List<AvailableViewInfo> cachedResource;

private List<AvailableViewInfo> get(
AbstractConfiguration configuration) {
if (cachedResource == null) {
cachedResource = loadClientMenuItems(configuration);
}
return cachedResource;
}

private void clear() {
cachedResource = null;
}
}

/**
* Collect views with menu annotation for automatic menu population. All
* client views are collected and any accessible server views.
Expand All @@ -82,10 +105,8 @@ public class MenuRegistry {
public static Map<String, AvailableViewInfo> collectMenuItems() {
Map<String, AvailableViewInfo> menuRoutes = MenuRegistry
.getMenuItems(true);
menuRoutes.entrySet()
.removeIf(entry -> Optional.ofNullable(entry.getValue())
.map(AvailableViewInfo::menu).map(MenuData::isExclude)
.orElse(false));
filterMenuItems(menuRoutes);

return menuRoutes;
}

Expand Down Expand Up @@ -115,7 +136,8 @@ public static List<AvailableViewInfo> collectMenuItemsList(Locale locale) {
return collectMenuItems().entrySet().stream().map(entry -> {
AvailableViewInfo value = entry.getValue();
return new AvailableViewInfo(value.title(), value.rolesAllowed(),
value.loginRequired(), entry.getKey(), value.lazy(),
value.loginRequired(),
getMenuLink(entry.getValue(), entry.getKey()), value.lazy(),
value.register(), value.menu(), value.children(),
value.routeParameters(), value.flowLayout());
}).sorted(getMenuOrderComparator(
Expand Down Expand Up @@ -303,41 +325,99 @@ public static Map<String, AvailableViewInfo> collectClientMenuItems(
boolean filterClientViews, AbstractConfiguration configuration,
VaadinRequest vaadinRequest) {

URL viewsJsonAsResource = getViewsJsonAsResource(configuration);
if (viewsJsonAsResource == null) {
LoggerFactory.getLogger(MenuRegistry.class).debug(
"No {} found under {} directory. Skipping client route registration.",
FILE_ROUTES_JSON_NAME,
configuration.isProductionMode() ? "'META-INF/VAADIN'"
: "'frontend/generated'");
return Collections.emptyMap();
}

Map<String, AvailableViewInfo> configurations = new HashMap<>();

try (InputStream source = viewsJsonAsResource.openStream()) {
if (source != null) {
ObjectMapper mapper = new ObjectMapper().configure(
DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,
false);
mapper.readValue(source,
new TypeReference<List<AvailableViewInfo>>() {
}).forEach(clientViewConfig -> collectClientViews("",
clientViewConfig, configurations));
}
} catch (IOException e) {
LoggerFactory.getLogger(MenuRegistry.class).warn(
"Failed load {} from {}", FILE_ROUTES_JSON_NAME,
viewsJsonAsResource.getPath(), e);
}
collectClientMenuItems(configuration).forEach(
viewInfo -> collectClientViews("", viewInfo, configurations));

if (filterClientViews) {
if (filterClientViews && !configurations.isEmpty()) {
filterClientViews(configurations, vaadinRequest);
}

return configurations;
}

/**
* Determines whether the application contains a Hilla automatic main
* layout.
* <p>
* This method detects only a top-level main layout, when the following
* conditions are met:
* <ul>
* <li>only one single root element is present in
* {@code file-routes.json}</li>
* <li>this element has no or blank {@code route} parameter</li>
* <li>this element has non-null children array, which may or may not be
* empty</li>
* </ul>
* <p>
* This method doesn't check nor does it detect the nested layouts, i.e.
* that are not root entries.
*
* @param configuration
* the {@link AbstractConfiguration} containing the application
* configuration
* @return {@code true} if a Hilla automatic main layout is present in the
* configuration, {@code false} otherwise
*/
public static boolean hasHillaMainLayout(
AbstractConfiguration configuration) {
List<AvailableViewInfo> viewInfos = collectClientMenuItems(
configuration);
return viewInfos.size() == 1
&& isMainLayout(viewInfos.iterator().next());
}

private static boolean isMainLayout(AvailableViewInfo viewInfo) {
return (viewInfo.route() == null || viewInfo.route().isBlank())
&& viewInfo.children() != null;
}

/**
* Caches the loaded file routes data in production. Always loads from a
* local file in development.
*
* @param configuration
* application configuration
* @return file routes data loaded from {@code file-routes.json}
*/
private static List<AvailableViewInfo> collectClientMenuItems(
AbstractConfiguration configuration) {
if (configuration.isProductionMode()) {
return FileRoutesCache.INSTANCE.get(configuration);
} else {
return loadClientMenuItems(configuration);
}
}

private static List<AvailableViewInfo> loadClientMenuItems(
AbstractConfiguration configuration) {
Objects.requireNonNull(configuration);
URL viewsJsonAsResource = getViewsJsonAsResource(configuration);
if (viewsJsonAsResource != null) {
try (InputStream source = viewsJsonAsResource.openStream()) {
if (source != null) {
ObjectMapper mapper = new ObjectMapper().configure(
DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,
false);
return mapper.readValue(source, new TypeReference<>() {
});
}
} catch (IOException e) {
LoggerFactory.getLogger(MenuRegistry.class).warn(
"Failed load {} from {}", FILE_ROUTES_JSON_NAME,
viewsJsonAsResource.getPath(), e);
}
} else {
LoggerFactory.getLogger(MenuRegistry.class).debug(
"No {} found under {} directory. Skipping client route registration.",
FILE_ROUTES_JSON_NAME,
configuration.isProductionMode() ? "'META-INF/VAADIN'"
: "'frontend/generated'");
}
return Collections.emptyList();
}

private static void collectClientViews(String basePath,
AvailableViewInfo viewConfig,
Map<String, AvailableViewInfo> configurations) {
Expand Down Expand Up @@ -437,6 +517,17 @@ private static void removeChildren(
}
}

private static boolean hasRequiredParameter(AvailableViewInfo viewInfo) {
final Map<String, RouteParamType> routeParameters = viewInfo
.routeParameters();
if (routeParameters != null && !routeParameters.isEmpty()
&& routeParameters.values().stream().anyMatch(
paramType -> paramType == RouteParamType.REQUIRED)) {
return true;
}
return false;
}

/**
* Check view against authentication state.
* <p>
Expand Down Expand Up @@ -484,6 +575,16 @@ public static boolean hasClientRoute(String route) {
return hasClientRoute(route, false);
}

/**
* For internal use only.
* <p>
* Clears file routes cache when running in production. Only used in tests
* and should not be needed in projects.
*/
public static void clearFileRoutesCache() {
FileRoutesCache.INSTANCE.clear();
}

/**
* See if there is a client route available for given route path, optionally
* excluding layouts (routes with children) from the check.
Expand Down Expand Up @@ -540,4 +641,37 @@ private static Comparator<AvailableViewInfo> getMenuOrderComparator(
: collator.compare(o1.route(), o2.route());
};
}

private static String getMenuLink(AvailableViewInfo info,
String defaultMenuLink) {
if (info.routeParameters() == null
|| info.routeParameters().isEmpty()) {
return (defaultMenuLink.startsWith("/")) ? defaultMenuLink
: "/" + defaultMenuLink;
}
// menu link with omitted route parameters
final var parameterNames = info.routeParameters().keySet();
return Stream.of(defaultMenuLink.split("/")).filter(
part -> parameterNames.stream().noneMatch(part::startsWith))
.collect(Collectors.joining("/"));
}

private static void filterMenuItems(
Map<String, AvailableViewInfo> menuRoutes) {
for (var path : new HashSet<>(menuRoutes.keySet())) {
if (!menuRoutes.containsKey(path)) {
continue;
}
var viewInfo = menuRoutes.get(path);
// Remove following, including nested ones:
// - routes with required parameters
// - routes with exclude=true
if (viewInfo.menu().isExclude() || hasRequiredParameter(viewInfo)) {
menuRoutes.remove(path);
if (viewInfo.children() != null) {
removeChildren(menuRoutes, viewInfo, path);
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ public interface AbstractConfiguration extends Serializable {
boolean isProductionMode();

/**
* Get if the dev server should be enabled. false by default as a
* development bundle is used.
* Get if the dev server should be enabled. Defaults to true if Hilla is in
* use, otherwise defaults to false to enable dev bundle.
*
* @return true if dev server should be used
* @deprecated Use {@link #getMode()} instead
Expand All @@ -53,9 +53,11 @@ public interface AbstractConfiguration extends Serializable {
default boolean frontendHotdeploy() {
if (isProductionMode()) {
return false;
} else if (FrontendUtils.isHillaUsed(getFrontendFolder())) {
return true;
} else {
return getBooleanProperty(InitParameters.FRONTEND_HOTDEPLOY, false);
}
return getBooleanProperty(InitParameters.FRONTEND_HOTDEPLOY,
FrontendUtils.isHillaUsed(getFrontendFolder()));
}

default File getFrontendFolder() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,11 @@ public boolean isProductionMode() {
@Override
public boolean frontendHotdeploy() {
if (isOwnProperty(FRONTEND_HOTDEPLOY)) {
return getBooleanProperty(FRONTEND_HOTDEPLOY,
FrontendUtils.isHillaUsed(getFrontendFolder()));
if (FrontendUtils.isHillaUsed(getFrontendFolder())) {
return true;
} else {
return getBooleanProperty(FRONTEND_HOTDEPLOY, false);
}
}
return parentConfig.frontendHotdeploy();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
{
"name": "flow-default-dependencies",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Artur Signell",
"license": "Apache-2.0",
"private": true,
"description": "A list of default Flow dependencies",
"dependencies": {
"@polymer/polymer": "3.5.2",
"@vaadin/common-frontend": "0.0.19",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
{
"name": "flow-default-react-router-dependencies",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "Apache-2.0",
"private": true,
"description": "A list of Flow dependencies when using React router",
"dependencies": {
"react": "18.3.1",
"react-dom": "18.3.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,8 @@
{
"name": "flow-vaadin-router-dependencies",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "Apache-2.0",
"private": true,
"description": "A list of Flow dependencies when using Vaadin router for Web Components",
"dependencies": {
"@vaadin/router": "2.0.0-rc4"
"@vaadin/router": "2.0.0"
},
"devDependencies": {
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"name": "flow-vite-dependencies",
"version": "1.0.0",
"description": "",
"private": true,
"description": "A ",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "react-function-location-plugin",
"name": "@vaadin/react-function-location-plugin",
"version": "1.0.0",
"description": "A Vite plugin for gather development information about source location of React functions",
"main": "react-function-location-plugin.js",
Expand Down
Loading

0 comments on commit dab3459

Please sign in to comment.