@vaadin/hilla-file-router
Version:
Hilla file-based router
305 lines • 9.39 kB
JavaScript
import { protectRoute } from "@vaadin/hilla-react-auth";
import { createElement } from "react";
import { createBrowserRouter } from "react-router";
import { convertComponentNameToTitle } from "../shared/convertComponentNameToTitle.js";
import { transformTree } from "../shared/transformTree.js";
function isReactRouteModule(module) {
return "default" in module && typeof module.default === "function" || "config" in module && typeof module.config === "object";
}
function createRouteKey(route) {
return `${route.path ?? ""}-${route.children ? "n" : "i"}`;
}
var RouteHandleFlags = function(RouteHandleFlags) {
RouteHandleFlags["FLOW_LAYOUT"] = "flowLayout";
RouteHandleFlags["IGNORE_FALLBACK"] = "ignoreFallback";
RouteHandleFlags["SKIP_LAYOUTS"] = "skipLayouts";
return RouteHandleFlags;
}(RouteHandleFlags || {});
function getRouteHandleFlag(route, flag) {
if (typeof route.handle === "object" && flag in route.handle) {
return route.handle[flag];
}
return undefined;
}
/**
* A builder for creating a Vaadin-specific router for React with
* authentication and server routes support.
*/
export class RouterConfigurationBuilder {
#modifiers = [];
/**
* Adds the given routes to the current list of routes. All the routes are
* deeply merged to preserve the path uniqueness.
*
* @param routes - A list of routes to add to the current list.
*/
withReactRoutes(routes) {
return this.update(routes);
}
/**
* Adds the given file routes to the current list of routes. All the routes
* are transformed to React RouterObjects and deeply merged to preserve the
* path uniqueness.
*
* @param routes - A list of routes to add to the current list.
*/
withFileRoutes(routes) {
return this.update(routes, ({ original, overriding: added, children }) => {
if (added) {
const { module, path, flowLayout } = added;
if (module && !isReactRouteModule(module)) {
throw new Error(`The module for the "${path}" section doesn't have the React component exported by default or a ViewConfig object exported as "config"`);
}
const element = module?.default ? createElement(module.default) : undefined;
const handle = {
...module?.config,
title: module?.config?.title ?? convertComponentNameToTitle(module?.default),
flowLayout: module?.config?.flowLayout ?? flowLayout
};
if (path === "" && !children) {
return {
...original,
element,
handle,
index: true
};
}
return {
...original,
path: module?.config?.route ?? path,
element,
children,
handle
};
}
return original;
});
}
/**
* Adds the given server route element to each branch of the current list of
* routes.
*
* @param component - The React component to add to each branch of the
* current list of routes.
* @param config - An optional configuration that will be applied to
* each fallback component.
*/
withFallback(component, config) {
this.withLayout(component);
const fallbackRoutes = [{
path: "*",
element: createElement(component),
handle: config
}, {
index: true,
element: createElement(component),
handle: config
}];
this.update(fallbackRoutes, ({ original, overriding: added, children, dupe }) => {
if (original && !getRouteHandleFlag(original, RouteHandleFlags.IGNORE_FALLBACK) && !dupe) {
if (!children) {
return original;
}
const _fallback = [...fallbackRoutes];
if (children.some(({ path }) => path === "*")) {
_fallback.shift();
}
if (children.some(({ index: i, path }) => i ?? path?.includes("?"))) {
_fallback.pop();
}
return {
...original,
children: [...children, ..._fallback]
};
}
return added;
});
return this;
}
/**
* Adds the layoutComponent as the parent layout to views with the flowLayouts ViewConfiguration set.
*
* @param layoutComponent - layout component to use, usually Flow
*/
withLayout(layoutComponent) {
this.#modifiers.push((originalRoutes) => {
if (!originalRoutes) {
return originalRoutes;
}
const result = transformTree(originalRoutes, null, (routes, next) => routes.reduce((lists, route) => {
const { server, client, ambivalent } = next(route.children ?? []);
const flag = getRouteHandleFlag(route, RouteHandleFlags.FLOW_LAYOUT);
if (flag === true) {
lists.server.push({
...route,
children: route.children ? [...server, ...ambivalent] : undefined
});
} else if (server.length > 0) {
lists.server.push({
...route,
children: route.children ? server : undefined
});
}
if (flag === false || client.length > 0) {
lists.client.push({
...route,
children: route.children ? client : undefined
});
}
if (flag === undefined && (lists.server.every(({ path }) => path !== route.path) || ambivalent.length > 0)) {
lists.ambivalent.push({
...route,
children: route.children ? ambivalent : undefined
});
}
return lists;
}, {
server: [],
client: [],
ambivalent: []
}));
return [
...result.server.length ? [{
element: createElement(layoutComponent),
children: result.server,
handle: { [RouteHandleFlags.IGNORE_FALLBACK]: true }
}] : [],
...result.client,
...result.ambivalent
];
});
return this;
}
/**
* Protects all the routes that require authentication. For more details see
* {@link @vaadin/hilla-react-auth#protectRoutes} function.
*
* @param redirectPath - the path to redirect to if the route is protected
* and the user is not authenticated.
*/
protect(redirectPath) {
this.update(undefined, ({ original: route, children }) => {
const finalRoute = protectRoute(route, redirectPath);
finalRoute.children = children;
return finalRoute;
});
return this;
}
update(routes, callback = ({ original, overriding, children }) => ({
...original,
...overriding,
children
})) {
this.#modifiers.push((existingRoutes) => transformTree([existingRoutes, routes], null, ([original, added], next) => {
if (original && added) {
const final = [];
const pathKeys = new Set([...original.map(createRouteKey), ...added.map(createRouteKey)]);
for (const pathKey of pathKeys) {
const originalRoutes = original.filter((r) => createRouteKey(r) === pathKey);
const addedRoutes = added.filter((r) => createRouteKey(r) === pathKey);
if (addedRoutes.length > 1) {
throw new Error("Adding multiple routes with the same path is not allowed");
}
const addedRoute = addedRoutes[0];
if (originalRoutes.length > 0 && addedRoute) {
for (let i = 0; i < originalRoutes.length; i++) {
final.push(callback({
original: originalRoutes[i],
overriding: addedRoute,
children: next([originalRoutes[i].children, addedRoute.children]),
dupe: i < originalRoutes.length - 1
}) ?? originalRoutes[i]);
}
} else if (originalRoutes.length > 0) {
for (let i = 0; i < originalRoutes.length; i++) {
final.push(callback({
original: originalRoutes[i],
children: next([originalRoutes[i].children, undefined]),
dupe: i < originalRoutes.length - 1
}) ?? originalRoutes[i]);
}
} else {
const result = callback({
overriding: addedRoute,
children: next([undefined, addedRoute.children]),
dupe: false
});
if (result) {
final.push(result);
}
}
}
return final.filter((r) => r != null);
} else if (original) {
return original.map((route) => callback({
original: route,
children: next([route.children, undefined]),
dupe: false
})).filter((r) => r != null);
} else if (added) {
return added.map((route) => callback({
overriding: route,
children: next([undefined, route.children]),
dupe: false
})).filter((r) => r != null);
}
return undefined;
}));
return this;
}
/**
* Builds the router with the current list of routes.
*/
build(options) {
this.#withLayoutSkipping();
const routes = this.#modifiers.reduce((acc, mod) => mod(acc) ?? acc, undefined) ?? [];
return {
routes,
router: createBrowserRouter([...routes], {
basename: new URL(document.baseURI).pathname,
...options
})
};
}
#withLayoutSkipping() {
this.#modifiers.push((originalRoutes) => {
if (!originalRoutes) {
return originalRoutes;
}
const result = transformTree(originalRoutes, null, (routes, next) => routes.reduce((lists, route) => {
if (getRouteHandleFlag(route, RouteHandleFlags.SKIP_LAYOUTS)) {
lists.skipped.push(route);
return lists;
}
if (!route.children?.length) {
lists.regular.push(route);
return lists;
}
const { skipped, regular } = next(route.children ?? []);
if (skipped.length > 0) {
const { element,...rest } = route;
lists.skipped.push({
...rest,
children: skipped
});
}
if (regular.length > 0) {
lists.regular.push({
...route,
children: regular
});
}
return lists;
}, {
skipped: [],
regular: []
}));
return [...result.skipped.length ? [{
children: result.skipped,
handle: { [RouteHandleFlags.IGNORE_FALLBACK]: true }
}] : [], ...result.regular];
});
return this;
}
}
//# sourceMappingURL=./RouterConfigurationBuilder.js.map