rasengan
Version:
The modern React Framework
371 lines (370 loc) • 13.4 kB
JavaScript
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;
};