Skip to content

Commit

Permalink
feat: Use a new RouterBuilder API for routes.tsx (#19020)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
mshabarov committed Apr 9, 2024
1 parent 90abc04 commit a07e2ad
Show file tree
Hide file tree
Showing 11 changed files with 336 additions and 259 deletions.
Expand Up @@ -533,7 +533,8 @@ public void mavenGoal_generateOpenApiJson_when_itIsInClientSideMode()
{
element: <MainLayout />,
handle: { title: 'Main' }
}
},
...serverSideRoutes
] as RouteObject[];
Expand All @@ -557,7 +558,8 @@ public void mavenGoal_generateTsFiles_when_enabled() throws Exception {
{
element: <MainLayout />,
handle: { title: 'Main' }
}
},
...serverSideRoutes
] as RouteObject[];
Expand Down
Expand Up @@ -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.
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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);
}
Expand All @@ -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) {
Expand Down
Expand Up @@ -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;

Expand Down Expand Up @@ -54,57 +55,64 @@ 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: <MainLayout />,
handle: { title: 'Main' },
children: [
{ path: '/hilla', element: <HillaView />, 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: <MainLayout />,
handle: { title: 'Main' },
children: [
{ path: '/', element: <HelloWorldView />, 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";
private static final String REACT_ADAPTER_TSX = "ReactAdapter.tsx";
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 <code>index.js</code> if necessary.
Expand All @@ -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);
Expand All @@ -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);
}
}
Expand Down Expand Up @@ -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) {
Expand All @@ -249,14 +232,19 @@ 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() {
return !options.getClassFinder().getAnnotatedClasses(Route.class)
.isEmpty();
}

private static boolean missingRoutesExport(String routesContent) {
return !ROUTES_EXPORT_PATTERN.matcher(routesContent).find();
}

private void writeFile(File target, String content)
throws ExecutionFailedException {

Expand Down
Expand Up @@ -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")
Expand Down Expand Up @@ -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;
};
Expand Up @@ -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 <RouterProvider router={router} />;
Expand Down
@@ -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()

0 comments on commit a07e2ad

Please sign in to comment.