UNPKG

rasengan

Version:

The modern React Framework

371 lines (370 loc) 13.4 kB
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime"; import { RouterProvider, createBrowserRouter, useLoaderData, useParams, } from 'react-router'; import { ErrorBoundary, NotFoundPageComponent, RasenganPageComponent, } from '../components/index.js'; import { Suspense } from 'react'; import MetadataProvider from '../providers/metadata.js'; const defaultMetadata = { title: 'Not Found', description: 'Page not found', }; /** * This function receives a router component and get a formated router first * and then return a router. */ export const getRouter = (routerInstance) => { const routes = generateRoutes(routerInstance); let router = createBrowserRouter(routes, { hydrationData: window.__staticRouterHydrationData, }); return () => _jsx(RouterProvider, { router: router }); }; /** * This function merge the metadata, giving priority to the ones comming from loader */ const mergeMetaData = (responseMeta, meta, isLayout = false) => { let mergedMetaData = { metaTags: [], links: [], openGraph: { url: '', image: '', }, twitter: { card: 'summary_large_image', image: '', title: '', }, }; if (!isLayout) { // merge title and description mergedMetaData['title'] = responseMeta.title ?? meta.title; mergedMetaData['description'] = responseMeta.description ?? meta.description; } // merge openGraph datas mergedMetaData['openGraph'] = { ...meta.openGraph, ...responseMeta.openGraph, }; // merge twitter data mergedMetaData['twitter'] = { ...meta.twitter, ...responseMeta.twitter, }; // merge elements of type <array> eg. metaTags and links const metaSet = new Set(); const linkSet = new Set(); if (meta['metaTags'] && Array.isArray(meta.metaTags)) { // Loop through the metaTags and add every key to the set for (const element of meta.metaTags) { metaSet.add(element.name ?? element.property); } if (responseMeta['metaTags'] && Array.isArray(responseMeta.metaTags)) { // Loop through the responseMeta and check if the key is already in the set for (const element of responseMeta.metaTags) { if (metaSet.has(element.name ?? element.property)) { // remove the element from the set metaSet.delete(element.name ?? element.property); } mergedMetaData.metaTags.push(element); } } // Loop through the remaining elements in the set for (const element of metaSet) { const metaElement = meta.metaTags.find((el) => el.name === element); if (metaElement) { mergedMetaData.metaTags.push(metaElement); } } } else { mergedMetaData.metaTags = responseMeta.metaTags ?? []; } if (meta['links'] && Array.isArray(meta.links)) { // Loop through the links and add every key to the set for (const element of meta.links) { linkSet.add(element.rel); } if (responseMeta['links'] && Array.isArray(responseMeta.links)) { // Loop through the responseMeta and check if the key is already in the set for (const element of responseMeta.links) { if (linkSet.has(element.rel)) { // remove the element from the set linkSet.delete(element.rel); } mergedMetaData.links.push(element); } } // Loop through the remaining elements in the set for (const element of linkSet) { const linkElement = meta.links.find((el) => el.rel === element); if (linkElement) { mergedMetaData.links.push(linkElement); } } } else { mergedMetaData.links = responseMeta.links ?? []; } return mergedMetaData; }; /** * This function create a loader function */ const createLoaderFunction = ({ loader, metadata, isLayout = false, }) => { return async ({ params, request }) => { try { // Check if the loader is defined if (!loader) { return { props: {}, meta: metadata, }; } // Get the response from the loader const response = await loader({ params, request }); // Handle redirection if (response.redirect) { const formData = new FormData(); formData.append('redirect', response.redirect); return new Response(formData, { status: 302, headers: { Location: response.redirect, }, }); } return { props: response.props, meta: mergeMetaData(response.meta ?? {}, metadata, isLayout), }; } catch (error) { console.error(error); return { props: {}, meta: { openGraph: { url: '', image: '', }, twitter: { card: 'summary_large_image', image: '', title: '', }, metaTags: [], links: [], }, }; } }; }; /** * This function receives a router component and return a formated router for static routing * @param router Represents the router component * @returns */ export const generateRoutes = (router, isRoot = true, parentLayout = undefined) => { // Initialization of the list of routes const routes = []; // Get information about the layout and the path const Layout = router.layout; const route = { path: !isRoot ? router.useParentLayout ? Layout.path.replace(parentLayout.path + '/', '') : Layout.path : Layout.path, errorElement: _jsx(ErrorBoundary, {}), Component() { // Default data const defaultData = { props: {}, }; // Get SSR data let { props } = useLoaderData() || defaultData; // get params const params = useParams(); const layoutProps = { ...props, params, }; // Check if the layout is the root layout and wrap it in a MetadataProvider if (isRoot || !router.useParentLayout) { // Generate metadata mapping const metadataMapping = generateMetadataMapping(router); return (_jsx(MetadataProvider, { metadataMapping: metadataMapping, children: _jsx(Layout, { ...layoutProps }) })); } return _jsx(Layout, { ...layoutProps }); }, async loader({ params, request }) { // Extract metadata from the layout const metadata = { openGraph: { url: '', image: '', }, twitter: { card: 'summary_large_image', image: '', title: '', }, ...Layout.metadata, }; return createLoaderFunction({ loader: Layout.loader, metadata, isLayout: true, })({ params, request, }); }, children: [], nested: router.useParentLayout, hydrateFallbackElement: _jsx(_Fragment, {}), // TODO: Add hydration fallback }; // Defining the page not found route if (isRoot || router.notFoundComponent) { route.children.push({ path: '*', element: router.notFoundComponent ?? _jsx(NotFoundPageComponent, {}), loader: async () => { return { props: {}, meta: defaultMetadata, }; }, }); } // Get informations about pages const pages = router.pages.map((Page) => { // /home => home // / => / const pagePathFormated = Page.path.startsWith('/') && Page.path !== '/' ? Page.path.slice(1) : Page.path; // Get the path of the page const path = Page.path === '/' ? Layout.path : Layout.path.length > 1 ? pagePathFormated : Page.path; return { path: path === Layout.path ? undefined : path, index: path === Layout.path, async loader({ params, request }) { // Extracting metadata from the page const metadata = { openGraph: { url: '', image: '', }, twitter: { card: 'summary_large_image', image: '', title: '', }, ...Page.metadata, }; return createLoaderFunction({ loader: Page.loader, metadata })({ params, request, }); }, Component() { // Default data const defaultData = { props: { params: {}, }, }; const loaderData = useLoaderData() || defaultData; return (_jsx(Suspense, { fallback: _jsx(_Fragment, { children: "Loading" }), children: _jsx(RasenganPageComponent, { page: Page, data: loaderData }) })); }, errorElement: _jsx(ErrorBoundary, {}), hydrateFallbackElement: _jsx(_Fragment, {}), // TODO: Add hydration fallback }; }); // Add pages into children of the current route pages.forEach((page) => { route.children.push(page); }); // Loop through imported routers in order to apply the same logic like above. for (const importedRouter of router.routers) { const importedRoutes = generateRoutes(importedRouter, false, Layout); for (const importedRoute of importedRoutes) { if (importedRoute.nested) { route.children.push(importedRoute); } else { routes.push(importedRoute); } } } // Make sure to add the route at the beginning of the list routes.unshift(route); // Return the formated router return routes; }; /** * This function receives a router component and return a mapping from path to metadata * @param router Represents the router component * @returns */ export const generateMetadataMapping = (router, isRoot = true, parentLayout = undefined) => { const metadataMapping = {}; // Get information about the layout and the path const Layout = router.layout; // Set default path layout if not provided if (!Layout.path) { throw new Error(`[rasengan] Page path is required for ${Layout.name} layout component`); } const layoutPath = !isRoot ? router.useParentLayout ? parentLayout.path + (Layout.path === '/' ? '' : Layout.path.startsWith('/') && parentLayout.path === '/' ? Layout.path.slice(1) : Layout.path) : Layout.path : Layout.path; // Get informations about pages router.pages.forEach((Page) => { // Set default page path if not provided if (!Page.path) { throw new Error(`[rasengan] Page path is required for ${Page.name} page component`); } const pagePathFormated = Page.path.startsWith('/') && Page.path !== '/' && layoutPath.endsWith('/') ? Page.path.slice(1) : Page.path; // Get the path of the page const path = Page.path === '/' ? layoutPath : layoutPath + pagePathFormated; // Get metadata metadataMapping[path] = { openGraph: { url: '', image: '', }, twitter: { card: 'summary_large_image', image: '', title: '', }, ...Page.metadata, }; }); // Loop through imported routers in order to apply the same logic like above. for (const importedRouter of router.routers) { const importedMetadataMapping = generateMetadataMapping(importedRouter, false, Layout); Object.assign(metadataMapping, importedMetadataMapping); // Add the metadata of the imported router's pages to the metadata mapping for (const [path, metadata] of Object.entries(importedMetadataMapping)) { metadataMapping[path] = metadata; } } return metadataMapping; };