UNPKG

@vaadin/hilla-file-router

Version:

Hilla file-based router

305 lines 9.39 kB
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