From a07e2ad4927e7a244659bedf59e826803a760613 Mon Sep 17 00:00:00 2001
From: Mikhail Shabarov <61410877+mshabarov@users.noreply.github.com>
Date: Tue, 9 Apr 2024 17:03:17 +0300
Subject: [PATCH] feat: Use a new RouterBuilder API for routes.tsx (#19020)
Removes the buildRoutes method and replaces it with a new RouterBuilder API that automatically injects server side routes where needed and adds the auth level.
Related-to vaadin/hilla#2238
Fixes #18988
---
.../plugin/maven/BuildFrontendMojoTest.java | 6 +-
.../flow/server/frontend/FrontendUtils.java | 32 +--
.../frontend/TaskGenerateReactFiles.java | 124 +++++-----
.../com/vaadin/flow/server/frontend/Flow.tsx | 28 +--
.../flow/server/frontend/index-react.tsx | 2 +-
.../flow/server/frontend/routes-flow.tsx | 19 ++
.../vaadin/flow/server/frontend/routes.tsx | 78 +++++--
.../server/frontend/FrontendUtilsTest.java | 54 ++---
.../frontend/TaskGenerateReactFilesTest.java | 217 +++++++++++-------
.../vite-basics/src/main/frontend/routes.tsx | 17 +-
.../src/main/frontend/routes.tsx | 18 +-
11 files changed, 336 insertions(+), 259 deletions(-)
create mode 100644 flow-server/src/main/resources/com/vaadin/flow/server/frontend/routes-flow.tsx
diff --git a/flow-plugins/flow-maven-plugin/src/test/java/com/vaadin/flow/plugin/maven/BuildFrontendMojoTest.java b/flow-plugins/flow-maven-plugin/src/test/java/com/vaadin/flow/plugin/maven/BuildFrontendMojoTest.java
index 7d0d21fbc5a..d1208542243 100644
--- a/flow-plugins/flow-maven-plugin/src/test/java/com/vaadin/flow/plugin/maven/BuildFrontendMojoTest.java
+++ b/flow-plugins/flow-maven-plugin/src/test/java/com/vaadin/flow/plugin/maven/BuildFrontendMojoTest.java
@@ -533,7 +533,8 @@ public void mavenGoal_generateOpenApiJson_when_itIsInClientSideMode()
{
element: ,
handle: { title: 'Main' }
- }
+ },
+ ...serverSideRoutes
] as RouteObject[];
@@ -557,7 +558,8 @@ public void mavenGoal_generateTsFiles_when_enabled() throws Exception {
{
element: ,
handle: { title: 'Main' }
- }
+ },
+ ...serverSideRoutes
] as RouteObject[];
diff --git a/flow-server/src/main/java/com/vaadin/flow/server/frontend/FrontendUtils.java b/flow-server/src/main/java/com/vaadin/flow/server/frontend/FrontendUtils.java
index 68070373742..c312b54e34f 100644
--- a/flow-server/src/main/java/com/vaadin/flow/server/frontend/FrontendUtils.java
+++ b/flow-server/src/main/java/com/vaadin/flow/server/frontend/FrontendUtils.java
@@ -237,9 +237,9 @@ public class FrontendUtils {
public static final String ROUTES_TSX = "routes.tsx";
- public static final String ROUTES_JS = "routes.js";
+ public static final String ROUTES_FLOW_TSX = "routes-flow.tsx";
- public static final String VIEWS_TS = "views.ts";
+ public static final String ROUTES_JS = "routes.js";
/**
* Default generated path for generated frontend files.
@@ -339,15 +339,6 @@ public class FrontendUtils {
private static final Pattern SERVER_SIDE_ROUTES_PATTERN = Pattern.compile(
"(?<=\\s|^)\\.{3}serverSideRoutes(?=\\s|$)", Pattern.MULTILINE);
- // Regex pattern matches "buildRoute("
- private static final Pattern BUILDROUTE_FUNCTION_PATTERN = Pattern.compile(
- "(?<=\\s|^)buildRoute\\((?=[\\s\\S]*|$)", Pattern.MULTILINE);
-
- // Regex pattern matches "buildRoute()"
- private static final Pattern BUILDROUTE_FUNCTION_NOARGS_PATTERN = Pattern
- .compile("(?<=\\s|^)buildRoute\\(\\)(?=[\\s\\S]*|$)",
- Pattern.MULTILINE);
-
// Regex pattern matches everything between "const|let|var routes = [" (or
// "const routes: RouteObject[] = [") and "...serverSideRoutes"
private static final Pattern CLIENT_SIDE_ROUTES_PATTERN = Pattern.compile(
@@ -1380,10 +1371,10 @@ private static boolean isRoutesContentUsingHillaViews(
private static boolean isRoutesTsxContentUsingHillaViews(
String routesContent) {
routesContent = StringUtil.removeComments(routesContent);
- // Note that here we assume that Frontend/views doesn't have views.
- // buildRoute() adds therefore only server side routes.
- if (hasBuildRouteFunction(routesContent)) {
- return !hasBuildRouteFunctionWithoutArguments(routesContent);
+ // Note that here we assume that Frontend/views doesn't have views and
+ // routes.tsx isn't the auto-generated one
+ if (hasFileOrReactRoutesFunction(routesContent)) {
+ return true;
}
return isRoutesContentUsingHillaViews(routesContent);
}
@@ -1392,13 +1383,10 @@ private static boolean missingServerSideRoutes(String routesContent) {
return !SERVER_SIDE_ROUTES_PATTERN.matcher(routesContent).find();
}
- private static boolean hasBuildRouteFunctionWithoutArguments(
- String routesContent) {
- return BUILDROUTE_FUNCTION_NOARGS_PATTERN.matcher(routesContent).find();
- }
-
- private static boolean hasBuildRouteFunction(String routesContent) {
- return BUILDROUTE_FUNCTION_PATTERN.matcher(routesContent).find();
+ private static boolean hasFileOrReactRoutesFunction(String routesContent) {
+ return !routesContent.isBlank()
+ && (routesContent.contains("withFileRoutes(")
+ || routesContent.contains("withReactRoutes("));
}
private static boolean mayHaveClientSideRoutes(String routesContent) {
diff --git a/flow-server/src/main/java/com/vaadin/flow/server/frontend/TaskGenerateReactFiles.java b/flow-server/src/main/java/com/vaadin/flow/server/frontend/TaskGenerateReactFiles.java
index 011838a9bbe..6a00dca0fda 100644
--- a/flow-server/src/main/java/com/vaadin/flow/server/frontend/TaskGenerateReactFiles.java
+++ b/flow-server/src/main/java/com/vaadin/flow/server/frontend/TaskGenerateReactFiles.java
@@ -27,6 +27,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.vaadin.flow.internal.StringUtil;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.server.ExecutionFailedException;
@@ -54,43 +55,41 @@ public class TaskGenerateReactFiles implements FallibleCommand {
public static final String CLASS_PACKAGE = "com/vaadin/flow/server/frontend/%s";
private Options options;
protected static String NO_IMPORT = """
- Faulty configuration of serverSideRoutes.
+ Faulty configuration of server-side routes.
The server route definition is missing from the '%1$s' file
To have working Flow routes add the following to the '%1$s' file:
- - import { buildRoute } from "Frontend/generated/flow/Flow";
- - call buildRoute optionally with routes and position for server side routes as shown below:
+ import Flow from 'Frontend/generated/flow/Flow';
+ import { RouterConfigurationBuilder } from '@vaadin/hilla-file-router/runtime.js';
+ export const { router, routes } = new RouterConfigurationBuilder()
+ .withFallback(Flow)
+ // .withFileRoutes() or .withReactRoutes()
+ // ...
+ .build();
- let routing = [
- {
- element: ,
- handle: { title: 'Main' },
- children: [
- { path: '/hilla', element: , handle: { title: 'Hilla' } }
- ],
- },
- ] as RouteObject[];
- export const routes = buildRoute(routing, routing[0].children);
- OR
- - import { serverSideRoutes } from "Frontend/generated/flow/Flow";
- - route '...serverSideRoutes' into the routes definition as shown below:
+ OR
+
+ import { createBrowserRouter, RouteObject } from 'react-router-dom';
+ import { serverSideRoutes } from 'Frontend/generated/flow/Flow';
+
+ function build() {
+ const routes = [...serverSideRoutes] as RouteObject[];
+ return {
+ router: createBrowserRouter(routes),
+ routes
+ };
+ }
+ export const { router, routes } = build();
- export const routes = [
- {
- element: ,
- handle: { title: 'Main' },
- children: [
- { path: '/', element: , handle: { title: 'Hello World' } },
- ...serverSideRoutes
- ],
- },
- ] as RouteObject[];
""";
protected static String MISSING_ROUTES_EXPORT = """
Routes need to be exported as 'routes' for server navigation handling.
- routes.tsx should at least contain
+ routes.tsx should contain
+ 'export const { router, routes } = new RouterConfigurationBuilder()
+ // routes building
+ .build();'
+ OR
'export const routes = [...serverSideRoutes] as RouteObject[];'
- but can have react routes also defined.
""";
private static final String FLOW_TSX = "Flow.tsx";
@@ -98,13 +97,22 @@ public class TaskGenerateReactFiles implements FallibleCommand {
static final String FLOW_FLOW_TSX = "flow/" + FLOW_TSX;
static final String FLOW_REACT_ADAPTER_TSX = "flow/" + REACT_ADAPTER_TSX;
private static final String ROUTES_JS_IMPORT_PATH_TOKEN = "%routesJsImportPath%";
- static final String VIEWS_TS_FALLBACK = """
- const routes = { path: "", module: undefined, children: [] };
- export default routes;
- """;
- private static Pattern SERVER_ROUTE_PATTERN = Pattern.compile(
- "import[\\s\\S]?\\{[\\s\\S]*(?:serverSideRoutes|buildRoute)+[\\s\\S]*\\}[\\s\\S]?from[\\s\\S]?(\"|'|`)Frontend\\/generated\\/flow\\/Flow(\\.js)?\\1;");
+ // matches setting the server-side routes from Flow.tsx:
+ // import { serverSideRoutes } from "Frontend/generated/flow/Flow";
+ private static final Pattern SERVER_ROUTE_PATTERN = Pattern.compile(
+ "import\\s+\\{[\\s\\S]*(?:serverSideRoutes)+[\\s\\S]*\\}\\s+from\\s+(\"|'|`)Frontend\\/generated\\/flow\\/Flow(\\.js)?\\1;[\\s\\S]+\\.{3}serverSideRoutes");
+
+ // matches setting the fallback component to RouterConfigurationBuilder,
+ // e.g. Flow component from Flow.tsx:
+ // import Flow from 'Frontend/generated/flow/Flow';
+ // ...
+ // .withFallback(Flow)
+ private static final Pattern FALLBACK_COMPONENT_PATTERN = Pattern.compile(
+ "import\\s+(\\w+)\\s+from\\s+(\"|'|`)Frontend\\/generated\\/flow\\/Flow(\\.js)?\\2;[\\s\\S]+withFallback\\(\\s*\\1\\s*\\)");
+
+ private static final Pattern ROUTES_EXPORT_PATTERN = Pattern.compile(
+ "export\\s+const\\s+(\\{[\\s\\S]*(router[\\s\\S]*routes|routes[\\s\\S]*router)[\\s\\S]*}|routes)");
/**
* Create a task to generate index.js
if necessary.
@@ -129,8 +137,6 @@ private void doExecute() throws ExecutionFailedException {
File frontendDirectory = options.getFrontendDirectory();
File frontendGeneratedFolder = options.getFrontendGeneratedFolder();
File flowTsx = new File(frontendGeneratedFolder, FLOW_FLOW_TSX);
- File viewsTs = new File(frontendGeneratedFolder,
- FrontendUtils.VIEWS_TS);
File reactAdapterTsx = new File(frontendGeneratedFolder,
FLOW_REACT_ADAPTER_TSX);
File routesTsx = new File(frontendDirectory, FrontendUtils.ROUTES_TSX);
@@ -141,21 +147,23 @@ private void doExecute() throws ExecutionFailedException {
if (fileAvailable(REACT_ADAPTER_TSX)) {
writeFile(reactAdapterTsx, getFileContent(REACT_ADAPTER_TSX));
}
- if (!viewsTs.exists()) {
- writeFile(viewsTs, VIEWS_TS_FALLBACK);
- }
if (!routesTsx.exists()) {
+ boolean isHillaUsed = FrontendUtils.isHillaUsed(
+ frontendDirectory, options.getClassFinder());
writeFile(frontendGeneratedFolderRoutesTsx,
- getFileContent(FrontendUtils.ROUTES_TSX));
+ getFileContent(isHillaUsed ? FrontendUtils.ROUTES_TSX
+ : FrontendUtils.ROUTES_FLOW_TSX));
} else {
String routesContent = FileUtils.readFileToString(routesTsx,
UTF_8);
+ routesContent = StringUtil.removeComments(routesContent);
+
if (missingServerRouteImport(routesContent)
&& serverRoutesAvailable()) {
throw new ExecutionFailedException(
String.format(NO_IMPORT, routesTsx.getPath()));
}
- if (!routesContent.contains("export const routes")) {
+ if (missingRoutesExport(routesContent)) {
throw new ExecutionFailedException(MISSING_ROUTES_EXPORT);
}
}
@@ -209,38 +217,13 @@ private void cleanup() throws ExecutionFailedException {
private String getFlowTsxFileContent(boolean frontendRoutesTsExists)
throws IOException {
- String content = getFileContent(FLOW_TSX).replace(
- ROUTES_JS_IMPORT_PATH_TOKEN,
+ return getFileContent(FLOW_TSX).replace(ROUTES_JS_IMPORT_PATH_TOKEN,
(frontendRoutesTsExists)
? FrontendUtils.FRONTEND_FOLDER_ALIAS
+ FrontendUtils.ROUTES_JS
: FrontendUtils.FRONTEND_FOLDER_ALIAS
+ FrontendUtils.GENERATED
+ FrontendUtils.ROUTES_JS);
- ;
- if (FrontendUtils.isHillaUsed(options.getFrontendDirectory(),
- options.getClassFinder())) {
- return content.replace("//%toReactRouterImport%",
- "import { toReactRouter } from '@vaadin/hilla-file-router/runtime.js';")
- .replace("//%viewsJsImport%",
- "import views from 'Frontend/generated/views.js';")
- .replace("//%buildRouteFunction%",
- """
- if(!routes) {
- // @ts-ignore
- const route: RouteObject = toReactRouter(views);
- if(route.children && route.children.length > 0) {
- serverSidePosition = route.children;
- if (route.element) {
- routes = [route];
- } else {
- routes = route.children;
- }
- }
- }
- """);
- }
- return content;
}
private boolean fileAvailable(String fileName) {
@@ -249,7 +232,8 @@ private boolean fileAvailable(String fileName) {
}
private boolean missingServerRouteImport(String routesContent) {
- return !SERVER_ROUTE_PATTERN.matcher(routesContent).find();
+ return !FALLBACK_COMPONENT_PATTERN.matcher(routesContent).find()
+ && !SERVER_ROUTE_PATTERN.matcher(routesContent).find();
}
private boolean serverRoutesAvailable() {
@@ -257,6 +241,10 @@ private boolean serverRoutesAvailable() {
.isEmpty();
}
+ private static boolean missingRoutesExport(String routesContent) {
+ return !ROUTES_EXPORT_PATTERN.matcher(routesContent).find();
+ }
+
private void writeFile(File target, String content)
throws ExecutionFailedException {
diff --git a/flow-server/src/main/resources/com/vaadin/flow/server/frontend/Flow.tsx b/flow-server/src/main/resources/com/vaadin/flow/server/frontend/Flow.tsx
index b3fffb1ea67..101031a6731 100644
--- a/flow-server/src/main/resources/com/vaadin/flow/server/frontend/Flow.tsx
+++ b/flow-server/src/main/resources/com/vaadin/flow/server/frontend/Flow.tsx
@@ -19,12 +19,9 @@ import {
matchRoutes,
NavigateFunction,
useLocation,
- useNavigate,
- RouteObject
+ useNavigate
} from "react-router-dom";
import { routes } from "%routesJsImportPath%";
-//%viewsJsImport%
-//%toReactRouterImport%
const flow = new _Flow({
imports: () => import("Frontend/generated/flow/generated-flow-imports.js")
@@ -460,26 +457,3 @@ export const createWebComponent = (tag: string, props?: Properties, onload?: ()
}
return React.createElement(tag);
};
-
-/**
- * Build routes for the application. Combines server side routes and FS routes.
- *
- * @param routes optional routes are for adding own route definition, giving routes will skip FS routes
- * @param serverSidePosition optional position where server routes should be put.
- * If non given they go to the root of the routes [].
- *
- * @returns RouteObject[] with combined routes
- */
-export const buildRoute = (routes?: RouteObject[], serverSidePosition?: RouteObject[]): RouteObject[] => {
- let combinedRoutes = [] as RouteObject[];
- //%buildRouteFunction%
- if(serverSidePosition) {
- serverSidePosition.push(...serverSideRoutes);
- } else {
- combinedRoutes.push(...serverSideRoutes);
- }
- if(routes) {
- combinedRoutes.push(...routes);
- }
- return combinedRoutes;
-};
\ No newline at end of file
diff --git a/flow-server/src/main/resources/com/vaadin/flow/server/frontend/index-react.tsx b/flow-server/src/main/resources/com/vaadin/flow/server/frontend/index-react.tsx
index 0dbc1eb5e3f..dbf1598c4b2 100644
--- a/flow-server/src/main/resources/com/vaadin/flow/server/frontend/index-react.tsx
+++ b/flow-server/src/main/resources/com/vaadin/flow/server/frontend/index-react.tsx
@@ -13,7 +13,7 @@
import { createElement } from 'react';
import { createRoot } from 'react-dom/client';
import { RouterProvider } from 'react-router-dom';
-import router from '%routesJsImportPath%';
+import { router } from '%routesJsImportPath%';
function App() {
return ;
diff --git a/flow-server/src/main/resources/com/vaadin/flow/server/frontend/routes-flow.tsx b/flow-server/src/main/resources/com/vaadin/flow/server/frontend/routes-flow.tsx
new file mode 100644
index 00000000000..186e35d0bff
--- /dev/null
+++ b/flow-server/src/main/resources/com/vaadin/flow/server/frontend/routes-flow.tsx
@@ -0,0 +1,19 @@
+/******************************************************************************
+ * This file is auto-generated by Vaadin.
+ * It configures React Router automatically by adding server-side (Flow) routes,
+ * which is enough for Vaadin Flow applications.
+ * Once any `.tsx` or `.jsx` React routes are added into
+ * `src/main/frontend/views/` directory, this route configuration is
+ * re-generated automatically by Vaadin.
+ ******************************************************************************/
+import { createBrowserRouter, RouteObject } from 'react-router-dom';
+import { serverSideRoutes } from 'Frontend/generated/flow/Flow';
+
+function build() {
+ const routes = [...serverSideRoutes] as RouteObject[];
+ return {
+ router: createBrowserRouter(routes),
+ routes
+ };
+}
+export const { router, routes } = build()
diff --git a/flow-server/src/main/resources/com/vaadin/flow/server/frontend/routes.tsx b/flow-server/src/main/resources/com/vaadin/flow/server/frontend/routes.tsx
index b1e0f6a0b1b..c1adca50388 100644
--- a/flow-server/src/main/resources/com/vaadin/flow/server/frontend/routes.tsx
+++ b/flow-server/src/main/resources/com/vaadin/flow/server/frontend/routes.tsx
@@ -1,18 +1,62 @@
-import { createBrowserRouter, RouteObject } from 'react-router-dom';
-import { buildRoute } from "Frontend/generated/flow/Flow";
+/******************************************************************************
+ * This file is auto-generated by Vaadin.
+ * It configures React Router automatically by looking for React views files,
+ * located in `src/main/frontend/views/` directory.
+ * A manual configuration can be done as well, you have to:
+ * - copy this file or create your own `routes.tsx` in your frontend directory,
+ * then modify this copied/created file. By default, the `routes.tsx` file
+ * should be in `src/main/frontend/` folder;
+ * - use `RouterConfigurationBuilder` API to configure routes for the application;
+ * - restart the application, so that the imports get re-generated.
+ *
+ * `RouterConfigurationBuilder` combines a File System-based route configuration
+ * or your explicit routes configuration with the server-side routes.
+ *
+ * It has the following methods:
+ * - `withFileRoutes` enables the File System-based routes autoconfiguration;
+ * - `withReactRoutes` adds manual explicit route hierarchy. Allows also to add
+ * an individual route, which then merged into File System-based routes,
+ * e.g. Log In view;
+ * - `withFallback` adds a given component, e.g. server-side routes,
+ * to each branch of the current list of routes;
+ * - `protect` optional method that adds an authentication later to the routes.
+ * May be used with no parameters or with a path to redirect to, if the user is
+ * not authenticated.
+ * - `build` terminal build operation that returns the final routes array
+ * RouterObject[] and router object.
+ *
+ * NOTE:
+ * - You need to restart the dev-server after adding the new `routes.tsx` file.
+ * After that, all modifications to `routes.tsx` are recompiled automatically.
+ * - You may need to change a routes import in `index.tsx`, if `index.tsx`
+ * exists in the frontend folder (not in generated folder) and you copied the file,
+ * as the import isn't updated automatically by Vaadin in this case.
+ ******************************************************************************/
+import { RouterConfigurationBuilder } from '@vaadin/hilla-file-router/runtime.js';
+import Flow from 'Frontend/generated/flow/Flow';
+import fileRoutes from 'Frontend/generated/file-routes.js';
-export const routes = buildRoute();
-
-// To define routes manually, use the following code as an example and remove the above code:
-// let routing = [
-// {
-// element: ,
-// handle: { title: 'Main' },
-// children: [
-// { path: '/hilla', element: , handle: { title: 'Hilla' } }
-// ],
-// },
-// ] as RouteObject[];
-// export const routes = buildRoute(routing, routing[0].children);
-
-export default createBrowserRouter(routes);
+export const { router, routes } = new RouterConfigurationBuilder()
+ .withFileRoutes(fileRoutes) // (1)
+ // To define routes manually or adding an individual route, use the
+ // following code and remove (1):
+ // .withReactRoutes(
+ // {
+ // element: ,
+ // handle: { title: 'Main' },
+ // children: [
+ // { path: '/hilla', element: , handle: { title: 'Hilla' } }
+ // ],
+ // },
+ // { path: '/login', element: , handle: { title: 'Login' } }
+ // )
+ // OR
+ // .withReactRoutes(
+ // { path: '/login', element: , handle: { title: 'Login' } },
+ // )
+ .withFallback(Flow)
+ // Optional method that adds an authentication for routes.
+ // Can take an optional path to redirect to, if not authenticated:
+ // .protect('/login');
+ .protect()
+ .build();
diff --git a/flow-server/src/test/java/com/vaadin/flow/server/frontend/FrontendUtilsTest.java b/flow-server/src/test/java/com/vaadin/flow/server/frontend/FrontendUtilsTest.java
index 473e6a24f45..3c629362fab 100644
--- a/flow-server/src/test/java/com/vaadin/flow/server/frontend/FrontendUtilsTest.java
+++ b/flow-server/src/test/java/com/vaadin/flow/server/frontend/FrontendUtilsTest.java
@@ -176,23 +176,29 @@ public class FrontendUtilsTest {
];
""";
- private static final String ROUTES_CONTENT_WITH_BUILD_ROUTE_TSX = """
- import { buildRoute } from "Frontend/generated/flow/Flow";
- export const routes = buildRoute();
+ private static final String ROUTES_CONTENT_WITH_WITH_FILE_ROUTES = """
+ import { RouterConfigurationBuilder } from '@vaadin/hilla-file-router/runtime.js';
+ import Flow from 'Frontend/generated/flow/Flow';
+ import fileRoutes from 'Frontend/generated/file-routes';
+
+ export const { router, routes } = new RouterConfigurationBuilder()
+ .withFileRoutes(fileRoutes)
+ .withFallback(Flow)
+ .build();
""";
- private static final String ROUTES_CONTENT_WITH_BUILD_ROUTE_WITH_ARGS_TSX = """
- import { buildRoute } from "Frontend/generated/flow/Flow";
- let routing: RouteObject[] = [
+ private static final String ROUTES_CONTENT_WITH_WITH_REACT_ROUTES = """
+ import { RouterConfigurationBuilder } from '@vaadin/hilla-file-router/runtime.js';
+ import Flow from 'Frontend/generated/flow/Flow';
+
+ export const { router, routes } = new RouterConfigurationBuilder()
+ .withReactRoutes([
{
element: ,
- handle: { title: 'Hilla CRM' },
- children: [
- ...serverSideRoutes
- ],
+ handle: { title: 'Hilla CRM' }
},
- ];
- export const routes = buildRoute(routing, routing[0].children);
+ ])
+ .withFallback(Flow).build();
""";
private static final String HILLA_VIEW_TSX = """
@@ -634,29 +640,19 @@ public void isHillaViewsUsed_serverSideRoutesMainLayoutTsx_true()
}
@Test
- public void isHillaViewsUsed_buildRouteTsxWithoutArgs_false()
- throws IOException {
- File frontend = prepareFrontendForRoutesFile(FrontendUtils.ROUTES_TSX,
- ROUTES_CONTENT_WITH_BUILD_ROUTE_TSX);
- Assert.assertFalse("hilla-views are not expected",
- FrontendUtils.isHillaViewsUsed(frontend));
- }
-
- @Test
- public void isHillaViewsUsed_buildRouteTsxWithoutArgsFSViewExists_false()
- throws IOException {
+ public void isHillaViewsUsed_withFileRoutes_true() throws IOException {
File frontend = prepareFrontendForRoutesFile(FrontendUtils.ROUTES_TSX,
- ROUTES_CONTENT_WITH_BUILD_ROUTE_TSX, true);
- Assert.assertTrue("hilla-views are expected",
+ ROUTES_CONTENT_WITH_WITH_FILE_ROUTES);
+ Assert.assertTrue("hilla-views are expected, as withFileRoutes is used",
FrontendUtils.isHillaViewsUsed(frontend));
}
@Test
- public void isHillaViewsUsed_buildRouteWithArgsTsx_true()
- throws IOException {
+ public void isHillaViewsUsed_withReactRoutes_true() throws IOException {
File frontend = prepareFrontendForRoutesFile(FrontendUtils.ROUTES_TSX,
- ROUTES_CONTENT_WITH_BUILD_ROUTE_WITH_ARGS_TSX);
- Assert.assertTrue("hilla-views are expected",
+ ROUTES_CONTENT_WITH_WITH_REACT_ROUTES);
+ Assert.assertTrue(
+ "hilla-views are expected, as withReactRoutes is used",
FrontendUtils.isHillaViewsUsed(frontend));
}
diff --git a/flow-server/src/test/java/com/vaadin/flow/server/frontend/TaskGenerateReactFilesTest.java b/flow-server/src/test/java/com/vaadin/flow/server/frontend/TaskGenerateReactFilesTest.java
index 134dc3f0522..c538f02bbd5 100644
--- a/flow-server/src/test/java/com/vaadin/flow/server/frontend/TaskGenerateReactFilesTest.java
+++ b/flow-server/src/test/java/com/vaadin/flow/server/frontend/TaskGenerateReactFilesTest.java
@@ -31,7 +31,6 @@
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.Tag;
-import com.vaadin.flow.di.Lookup;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.server.ExecutionFailedException;
import com.vaadin.flow.server.frontend.scanner.ClassFinder;
@@ -86,33 +85,35 @@ public void reactFilesAreWrittenToFrontend()
}
@Test
- public void routesContainImport_serverSideRoutes_noExceptionThrown()
+ public void routesContainImportAndUsage_serverSideRoutes_noExceptionThrown()
throws IOException, ExecutionFailedException {
String content = """
import HelloWorldView from 'Frontend/views/helloworld/HelloWorldView.js';
import MainLayout from 'Frontend/views/MainLayout.js';
import { lazy } from 'react';
- import { createBrowserRouter, RouteObject } from 'react-router-dom';
- import {serverSideRoutes} from "Frontend/generated/flow/Flow";
+ import { RouterConfigurationBuilder } from '@vaadin/hilla-file-router/runtime.js';
+ import Flow from 'Frontend/generated/flow/Flow';
import {protectRoutes} from "@hilla/react-auth";
import LoginView from "Frontend/views/LoginView";
const AboutView = lazy(async () => import('Frontend/views/about/AboutView.js'));
-
- export const routes: RouteObject[] = protectRoutes([
- {
- element: ,
- handle: { title: 'Main' },
- children: [
- { path: '/', element: , handle: { title: 'Hello World', rolesAllowed: ['USER'] } },
- { path: '/about', element: , handle: { title: 'About' } },
- ...serverSideRoutes
- ],
- },
- { path: '/login', element: },
- ]);
-
- export default createBrowserRouter(routes);
+ export const { router, routes } = new RouterConfigurationBuilder()
+ .withReactRoutes([
+ {
+ element: ,
+ handle: { title: 'Main' },
+ children: [
+ { path: '/', element: , handle: { title: 'Hello World', rolesAllowed: ['USER'] } },
+ { path: '/about', element: , handle: { title: 'About' } },
+ ],
+ },
+ ])
+ .withFallback(Flow)
+ .withReactRoutes([
+ { path: '/login', element: , handle: { title: 'Login' } },
+ ])
+ .protect()
+ .build();
""";
FileUtils.write(routesTsx, content, StandardCharsets.UTF_8);
@@ -123,31 +124,39 @@ public void routesContainImport_serverSideRoutes_noExceptionThrown()
}
@Test
- public void routesContainImport_buildRoutes_noExceptionThrown()
- throws IOException, ExecutionFailedException {
+ public void routesContainOnlyImport_serverSideRoutes_exceptionThrown()
+ throws IOException {
String content = """
- import HelloWorldView from 'Frontend/views/helloworld/HelloWorldView.js';
- import MainLayout from 'Frontend/views/MainLayout.js';
- import { createBrowserRouter, RouteObject } from 'react-router-dom';
- import { buildRoutes } from "Frontend/generated/flow/Flow";
- import LoginView from "Frontend/views/LoginView";
+ import { serverSideRoutes } from 'Frontend/generated/flow/Flow';
+ """;
- const AboutView = lazy(async () => import('Frontend/views/about/AboutView.js'));
+ FileUtils.write(routesTsx, content, StandardCharsets.UTF_8);
- export const routes: RouteObject[] = buildRoutes(
- [
- { path: '/', element: , handle: { title: 'Hello World', rolesAllowed: ['USER'] } },
- { path: '/about', element: , handle: { title: 'About' } }
- ]);
+ TaskGenerateReactFiles task = new TaskGenerateReactFiles(options);
- export default createBrowserRouter(routes);
+ Exception exception = Assert.assertThrows(
+ ExecutionFailedException.class, () -> task.execute());
+ Assert.assertEquals(String.format(TaskGenerateReactFiles.NO_IMPORT,
+ routesTsx.getPath()), exception.getMessage());
+ }
+
+ @Test
+ public void routesContainNoImport_serverSideRoutes_exceptionThrown()
+ throws IOException {
+ String content = """
+ export const routes = [
+ ...serverSideRoutes
+ ] as RouteObject[];
""";
FileUtils.write(routesTsx, content, StandardCharsets.UTF_8);
TaskGenerateReactFiles task = new TaskGenerateReactFiles(options);
- task.execute();
+ Exception exception = Assert.assertThrows(
+ ExecutionFailedException.class, () -> task.execute());
+ Assert.assertEquals(String.format(TaskGenerateReactFiles.NO_IMPORT,
+ routesTsx.getPath()), exception.getMessage());
}
@Test
@@ -157,7 +166,7 @@ public void routesContainMultipleFlowImports_noExceptionThrown()
import HelloWorldView from 'Frontend/views/helloworld/HelloWorldView.js';
import MainLayout from 'Frontend/views/MainLayout.js';
import { createBrowserRouter, RouteObject } from 'react-router-dom';
- import { tea, buildRoutes, serverSideRoutes, coffee } from "Frontend/generated/flow/Flow";
+ import { tea, serverSideRoutes, coffee } from "Frontend/generated/flow/Flow";
import LoginView from "Frontend/views/LoginView";
const AboutView = lazy(async () => import('Frontend/views/about/AboutView.js'));
@@ -186,7 +195,7 @@ public void routesContainMultipleFlowImports_noExceptionThrown()
}
@Test
- public void routesMissingImport_noBuildOrServerSideRoutes_exceptionThrown()
+ public void routesMissingImportAndUsage_noBuildOrServerSideRoutes_exceptionThrown()
throws IOException, ExecutionFailedException {
String content = """
import HelloWorldView from 'Frontend/views/helloworld/HelloWorldView.js';
@@ -223,7 +232,7 @@ public void routesMissingImport_noBuildOrServerSideRoutes_exceptionThrown()
}
@Test
- public void routesMissingImport_expectionThrown() throws IOException {
+ public void routesMissingImport_exceptionThrown() throws IOException {
String content = """
import HelloWorldView from 'Frontend/views/helloworld/HelloWorldView.js';
import MainLayout from 'Frontend/views/MainLayout.js';
@@ -260,14 +269,13 @@ public void routesMissingImport_expectionThrown() throws IOException {
}
@Test
- public void routesContainsRoutesExport_noExceptionThrown()
+ public void missingImport_noServerRoutesDefined_noExceptionThrown()
throws IOException, ExecutionFailedException {
String content = """
import HelloWorldView from 'Frontend/views/helloworld/HelloWorldView.js';
import MainLayout from 'Frontend/views/MainLayout.js';
import { lazy } from 'react';
import { createBrowserRouter, RouteObject } from 'react-router-dom';
- import {serverSideRoutes} from "Frontend/generated/flow/Flow";
import {protectRoutes} from "@hilla/react-auth";
import LoginView from "Frontend/views/LoginView";
@@ -279,8 +287,7 @@ public void routesContainsRoutesExport_noExceptionThrown()
handle: { title: 'Main' },
children: [
{ path: '/', element: , handle: { title: 'Hello World', rolesAllowed: ['USER'] } },
- { path: '/about', element: , handle: { title: 'About' } },
- ...serverSideRoutes
+ { path: '/about', element: , handle: { title: 'About' } }
],
},
{ path: '/login', element: },
@@ -291,76 +298,105 @@ public void routesContainsRoutesExport_noExceptionThrown()
FileUtils.write(routesTsx, content, StandardCharsets.UTF_8);
+ Mockito.when(classFinder.getAnnotatedClasses(Route.class))
+ .thenReturn(Collections.emptySet());
+
TaskGenerateReactFiles task = new TaskGenerateReactFiles(options);
task.execute();
}
@Test
- public void missingImport_noServerRoutesDefined_noExceptionThrown()
- throws IOException, ExecutionFailedException {
+ public void routesExportMissing_exceptionThrown() throws IOException {
String content = """
import HelloWorldView from 'Frontend/views/helloworld/HelloWorldView.js';
import MainLayout from 'Frontend/views/MainLayout.js';
import { lazy } from 'react';
- import { createBrowserRouter, RouteObject } from 'react-router-dom';
+ import { RouterConfigurationBuilder } from '@vaadin/hilla-file-router/runtime.js';
+ import Flow from 'Frontend/generated/flow/Flow';
import {protectRoutes} from "@hilla/react-auth";
import LoginView from "Frontend/views/LoginView";
const AboutView = lazy(async () => import('Frontend/views/about/AboutView.js'));
-
- export const routes: RouteObject[] = protectRoutes([
- {
- element: ,
- handle: { title: 'Main' },
- children: [
- { path: '/', element: , handle: { title: 'Hello World', rolesAllowed: ['USER'] } },
- { path: '/about', element: , handle: { title: 'About' } }
- ],
- },
- { path: '/login', element: },
- ]);
-
- export default createBrowserRouter(routes);
+ export const { router } = new RouterConfigurationBuilder()
+ .withReactRoutes([
+ {
+ element: ,
+ handle: { title: 'Main' },
+ children: [
+ { path: '/', element: , handle: { title: 'Hello World', rolesAllowed: ['USER'] } },
+ { path: '/about', element: , handle: { title: 'About' } },
+ ],
+ },
+ ])
+ .withFallback(Flow)
+ .withReactRoutes([
+ { path: '/login', element: , handle: { title: 'Login' } },
+ ])
+ .protect()
+ .build();
""";
FileUtils.write(routesTsx, content, StandardCharsets.UTF_8);
- Mockito.when(classFinder.getAnnotatedClasses(Route.class))
- .thenReturn(Collections.emptySet());
-
TaskGenerateReactFiles task = new TaskGenerateReactFiles(options);
- task.execute();
+ Exception exception = Assert.assertThrows(
+ ExecutionFailedException.class, () -> task.execute());
+ Assert.assertEquals(TaskGenerateReactFiles.MISSING_ROUTES_EXPORT,
+ exception.getMessage());
}
@Test
- public void routesexportMissing_expectionThrown() throws IOException {
+ public void withFallbackMissing_exceptionThrown() throws IOException {
String content = """
import HelloWorldView from 'Frontend/views/helloworld/HelloWorldView.js';
import MainLayout from 'Frontend/views/MainLayout.js';
import { lazy } from 'react';
- import { createBrowserRouter, RouteObject } from 'react-router-dom';
- import {serverSideRoutes} from "Frontend/generated/flow/Flow";
+ import { RouterConfigurationBuilder } from '@vaadin/hilla-file-router/runtime.js';
+ import Flow from 'Frontend/generated/flow/Flow';
import {protectRoutes} from "@hilla/react-auth";
import LoginView from "Frontend/views/LoginView";
const AboutView = lazy(async () => import('Frontend/views/about/AboutView.js'));
+ export const { router, routes } = new RouterConfigurationBuilder()
+ .withReactRoutes([
+ {
+ element: ,
+ handle: { title: 'Main' },
+ children: [
+ { path: '/', element: , handle: { title: 'Hello World', rolesAllowed: ['USER'] } },
+ { path: '/about', element: , handle: { title: 'About' } },
+ ],
+ },
+ ])
+ .withReactRoutes([
+ { path: '/login', element: , handle: { title: 'Login' } },
+ ])
+ .protect()
+ .build();
+ """;
- const routes: RouteObject[] = protectRoutes([
- {
- element: ,
- handle: { title: 'Main' },
- children: [
- { path: '/', element: , handle: { title: 'Hello World', rolesAllowed: ['USER'] } },
- { path: '/about', element: , handle: { title: 'About' } },
- ...serverSideRoutes
- ],
- },
- { path: '/login', element: },
- ]);
+ FileUtils.write(routesTsx, content, StandardCharsets.UTF_8);
- export default createBrowserRouter(routes);
+ TaskGenerateReactFiles task = new TaskGenerateReactFiles(options);
+
+ Exception exception = Assert.assertThrows(
+ ExecutionFailedException.class, () -> task.execute());
+ Assert.assertEquals(String.format(TaskGenerateReactFiles.NO_IMPORT,
+ routesTsx.getPath()), exception.getMessage());
+ }
+
+ @Test
+ public void withFallbackReceivesDifferentObject_exceptionThrown()
+ throws IOException {
+ String content = """
+ import { RouterConfigurationBuilder } from '@vaadin/hilla-file-router/runtime.js';
+ import foo from 'Frontend/generated/flow/Flow';
+
+ export const { router, routes } = new RouterConfigurationBuilder()
+ .withFallback(Flow)
+ .build();
""";
FileUtils.write(routesTsx, content, StandardCharsets.UTF_8);
@@ -369,8 +405,29 @@ public void routesexportMissing_expectionThrown() throws IOException {
Exception exception = Assert.assertThrows(
ExecutionFailedException.class, () -> task.execute());
- Assert.assertEquals(TaskGenerateReactFiles.MISSING_ROUTES_EXPORT,
- exception.getMessage());
+ Assert.assertEquals(String.format(TaskGenerateReactFiles.NO_IMPORT,
+ routesTsx.getPath()), exception.getMessage());
+ }
+
+ @Test
+ public void withFallbackMissesImport_exceptionThrown() throws IOException {
+ String content = """
+ import { RouterConfigurationBuilder } from '@vaadin/hilla-file-router/runtime.js';
+
+ const AboutView = lazy(async () => import('Frontend/views/about/AboutView.js'));
+ export const { router, routes } = new RouterConfigurationBuilder()
+ .withFallback(Flow)
+ .build();
+ """;
+
+ FileUtils.write(routesTsx, content, StandardCharsets.UTF_8);
+
+ TaskGenerateReactFiles task = new TaskGenerateReactFiles(options);
+
+ Exception exception = Assert.assertThrows(
+ ExecutionFailedException.class, () -> task.execute());
+ Assert.assertEquals(String.format(TaskGenerateReactFiles.NO_IMPORT,
+ routesTsx.getPath()), exception.getMessage());
}
@Test
diff --git a/flow-tests/test-frontend/vite-basics/src/main/frontend/routes.tsx b/flow-tests/test-frontend/vite-basics/src/main/frontend/routes.tsx
index 183b4813a99..1163597e694 100644
--- a/flow-tests/test-frontend/vite-basics/src/main/frontend/routes.tsx
+++ b/flow-tests/test-frontend/vite-basics/src/main/frontend/routes.tsx
@@ -2,9 +2,14 @@ import { createBrowserRouter, RouteObject } from 'react-router-dom';
import { serverSideRoutes } from 'Frontend/generated/flow/Flow';
import ReactComponents from './ReactComponents';
-export const routes = [
- { path: 'react-components', element: },
- ...serverSideRoutes
-] as RouteObject[];
-
-export default createBrowserRouter(routes);
+function build() {
+ const routes = [
+ { path: 'react-components', element: },
+ ...serverSideRoutes
+ ] as RouteObject[];
+ return {
+ router: createBrowserRouter(routes),
+ routes
+ };
+}
+export const { router, routes } = build()
diff --git a/flow-tests/test-react-router/src/main/frontend/routes.tsx b/flow-tests/test-react-router/src/main/frontend/routes.tsx
index 8f19b38777d..c7a047e0efd 100644
--- a/flow-tests/test-react-router/src/main/frontend/routes.tsx
+++ b/flow-tests/test-react-router/src/main/frontend/routes.tsx
@@ -2,11 +2,15 @@ import ReactView from "Frontend/ReactView";
import { serverSideRoutes } from "Frontend/generated/flow/Flow";
import { createBrowserRouter, RouteObject } from "react-router-dom";
-
-export const routes = [
- {path: '/react', element: , handle: { title: 'React Test View' }},
- ...serverSideRoutes
-] as RouteObject[];
-
-export default createBrowserRouter([...routes], { basename: new URL(document.baseURI).pathname });
+function build() {
+ const routes = [
+ {path: '/react', element: , handle: { title: 'React Test View' }},
+ ...serverSideRoutes
+ ] as RouteObject[];
+ return {
+ router: createBrowserRouter([...routes], { basename: new URL(document.baseURI).pathname }),
+ routes
+ };
+}
+export const { router, routes } = build()