Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Use a new RouterBuilder API for routes.tsx #19020

Merged
merged 36 commits into from
Apr 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
8a3dd4c
feat: Use a new RouterBuilder API for routes.tsx
mshabarov Mar 22, 2024
ad8f919
Merge branch 'main' into new-route-builder-api
mshabarov Mar 22, 2024
29de181
fix import
mshabarov Mar 22, 2024
624ca16
Merge remote-tracking branch 'origin/new-route-builder-api' into new-…
mshabarov Mar 22, 2024
af29624
fix import
mshabarov Mar 22, 2024
7d3ce09
fix tests
mshabarov Mar 22, 2024
9bd57e6
fix tests
mshabarov Mar 22, 2024
1fc450d
update routes.tsx in vite-basics
mshabarov Mar 25, 2024
8ce13f7
Merge branch 'main' into new-route-builder-api
mshabarov Mar 25, 2024
e74e612
address review comments
mshabarov Mar 25, 2024
8990d9f
Merge remote-tracking branch 'origin/new-route-builder-api' into new-…
mshabarov Mar 25, 2024
9b218f8
Merge branch 'main' into new-route-builder-api
mshabarov Mar 25, 2024
5ebec47
revert changes in BuildFrontendMojoTest
mshabarov Mar 26, 2024
cb37a19
Merge remote-tracking branch 'origin/new-route-builder-api' into new-…
mshabarov Mar 26, 2024
66f9916
Merge branch 'main' into new-route-builder-api
mshabarov Mar 26, 2024
4c7eaee
Merge remote-tracking branch 'origin/main' into new-route-builder-api
mshabarov Mar 28, 2024
f039652
Merge remote-tracking branch 'origin/new-route-builder-api' into new-…
mshabarov Mar 28, 2024
8cdd04b
Rename to withFallbackComponent, more docs for protect, extra pattern…
mshabarov Apr 2, 2024
1e2d831
Merge branch 'main' into new-route-builder-api
mshabarov Apr 2, 2024
50031dc
Simplifies missing route logic, more clarifications to routes.tsx tem…
mshabarov Apr 2, 2024
90d69db
Merge remote-tracking branch 'origin/new-route-builder-api' into new-…
mshabarov Apr 2, 2024
41afd50
Merge branch 'main' into new-route-builder-api
mshabarov Apr 2, 2024
760f5dd
Merge branch 'main' into new-route-builder-api
mshabarov Apr 2, 2024
fee3c8f
fix test
mshabarov Apr 2, 2024
956caf6
Merge remote-tracking branch 'origin/new-route-builder-api' into new-…
mshabarov Apr 2, 2024
a0f4f92
Merge branch 'main' into new-route-builder-api
mshabarov Apr 3, 2024
47cd498
adapt to a new changes in Hilla
mshabarov Apr 4, 2024
b455eec
Merge branch 'main' into new-route-builder-api
mshabarov Apr 5, 2024
14eede8
formatting
mshabarov Apr 5, 2024
08c75f4
Merge remote-tracking branch 'origin/new-route-builder-api' into new-…
mshabarov Apr 5, 2024
d5f6ab0
Merge branch 'main' into new-route-builder-api
mshabarov Apr 5, 2024
c45e157
Merge branch 'main' into new-route-builder-api
mshabarov Apr 8, 2024
1238e1c
changed import for router
mshabarov Apr 8, 2024
d4c4489
export router properly and update builder name
mshabarov Apr 9, 2024
b021791
fix routes config in test module
mshabarov Apr 9, 2024
b668bfd
fix vite-basics module
mshabarov Apr 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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;
};
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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()