fixing so much trouble

This commit is contained in:
Maulana Sodiqin
2025-05-23 00:28:29 +07:00
parent 376e646c10
commit 9a0fee65a5
61 changed files with 735 additions and 362 deletions
@@ -1,15 +1,22 @@
import { lazy, LazyExoticComponent, ReactElement } from 'react';
import { lazy, LazyExoticComponent, ReactNode } from 'react';
import { ActionFunction, LoaderFunction, RouteObject } from 'react-router';
type TPermissionItem = {
id: string;
name: string;
created_at: string;
updated_at: string;
};
interface PageModuleExports {
default: () => ReactElement;
default: () => ReactNode;
loader?: LoaderFunction;
action?: ActionFunction;
permissions?: Array<string>;
}
interface LoadingModuleExports {
default: () => ReactElement;
default: () => ReactNode;
}
interface RouteHandle {
@@ -38,7 +45,6 @@ export function convertPagesToRoute(
filePath,
loadingFiles
);
const route = createRoute({
PageComponent: page,
LoadingComponent: loadingComponent,
@@ -51,6 +57,20 @@ export function convertPagesToRoute(
const result = (await importer()) as PageModuleExports;
return 'loader' in result ? result.loader?.(args) : null;
},
async guard() {
const result = (await importer()) as PageModuleExports;
const localStoragePermission = localStorage.getItem('permissions');
const permissions: TPermissionItem[] | undefined =
localStoragePermission
? JSON.parse(localStoragePermission)
: undefined;
return 'permissions' in result
? result.permissions?.every(
(permission) =>
permissions?.some((item) => permission === item.name) || false
) || false
: true;
},
});
routes = mergeRoutes(routes, route);
});
@@ -62,19 +82,14 @@ function findMatchingLoadingComponent(
loadingFiles: Record<string, () => Promise<unknown>>
) {
const loadingPath = filePath.replace(/(page|layout)\.tsx$/, 'loading.tsx');
const groupMatch = filePath.match(/\([^/]+\//);
const groupLoadingPath = groupMatch ? `/${groupMatch[0]}loading.tsx` : null;
const globalLoadingPath = './app/loading.tsx';
const loader =
loadingFiles[loadingPath] ||
(groupLoadingPath && loadingFiles[groupLoadingPath]) ||
loadingFiles[globalLoadingPath];
if (!loader) return undefined;
return lazy(loader as () => Promise<LoadingModuleExports>);
}
@@ -86,68 +101,110 @@ function mergeRoutes(
throw new Error(
`Paths do not match: "${target.path}" and "${source.path}"`
);
target.children = target.children || [];
if (source.handle?.pageType === 'layout') {
return handleLayoutMerge(target, source);
}
if (source.handle?.pageType === 'page') {
return handlePageMerge(target, source);
}
if (source.children && source.children.length > 0) {
if (target.handle?.pageType === 'page') {
if (!target.children?.some((child) => child.index)) {
target.children = target.children || [];
target.children.unshift({
index: true,
element: target.element,
HydrateFallback: target.HydrateFallback,
action: target.action,
loader: target.loader,
handle: target.handle,
errorElement: target.errorElement,
});
}
delete target.element;
delete target.action;
delete target.loader;
delete target.handle;
}
mergeChildRoutes(target, source);
}
return target;
}
export function mergeChildRoutes(
target: ExtendedRouteObject,
source: ExtendedRouteObject
): void {
if (!source.children) return;
if (!target.children) {
target.children = [];
}
source.children.forEach((sourceChild) => {
const matchingChild = target.children!.find(
(targetChild) => targetChild.path === sourceChild.path
);
if (source.handle?.pageType === 'layout') {
if (!target.element) {
target.element = source.element;
target.HydrateFallback = source.HydrateFallback;
target.action = source.action;
target.loader = source.loader;
target.handle = source.handle;
target.errorElement = source.errorElement;
target.children = target.children ?? [];
} else if (target.handle?.pageType === 'page') {
target = swapTargetRouteAsIndexRouteAndUpdateWithRoute(target, source);
if (matchingChild) {
mergeRoutes(matchingChild, sourceChild);
} else {
target.children!.push(sourceChild);
}
return target;
}
});
}
if (
source.handle?.pageType === 'page' &&
!target.children.some((child) => child.index)
) {
target.children.unshift({
index: true,
export function handleLayoutMerge(
target: ExtendedRouteObject,
source: ExtendedRouteObject
): ExtendedRouteObject {
if (!target.element) {
Object.assign(target, {
element: source.element,
HydrateFallback: source.HydrateFallback,
action: source.action,
loader: source.loader,
handle: source.handle,
errorElement: source.errorElement,
});
return target;
} else if (target.handle?.pageType === 'page') {
target = swapTargetRouteAsIndexRouteAndUpdateWithRoute(target, source);
}
if (
target.handle?.pageType === 'layout' &&
source.handle?.pageType === 'page'
) {
target = addRouteAsIndexRouteForTargetRoute(target, source);
return target;
}
if (source.children) {
target.children = target.children ?? [];
source.children.forEach((sourceChild) => {
const matchingChild = target.children?.find(
(targetChild) => targetChild.path === sourceChild.path
);
if (matchingChild) mergeRoutes(matchingChild, sourceChild);
else target.children?.push(sourceChild);
});
}
return target;
}
function swapTargetRouteAsIndexRouteAndUpdateWithRoute(
export function handlePageMerge(
target: ExtendedRouteObject,
route: ExtendedRouteObject
source: ExtendedRouteObject
): ExtendedRouteObject {
target.children = target.children ?? [];
if (!target.children) {
target.children = [];
}
if (
!target.children.some((child) => child.index) ||
target.handle?.pageType === 'layout'
) {
if (target.handle?.pageType === 'layout') {
addRouteAsIndexRouteForTargetRoute(target, source);
} else {
target.children.unshift({
index: true,
element: source.element,
HydrateFallback: source.HydrateFallback,
action: source.action,
loader: source.loader,
handle: source.handle,
errorElement: source.errorElement,
});
}
}
return target;
}
export function swapTargetRouteAsIndexRouteAndUpdateWithRoute(
target: ExtendedRouteObject,
layout: ExtendedRouteObject
): ExtendedRouteObject {
target.children = target.children || [];
target.children.push({
index: true,
element: target.element,
@@ -157,38 +214,38 @@ function swapTargetRouteAsIndexRouteAndUpdateWithRoute(
handle: target.handle,
errorElement: target.errorElement,
});
target.element = route.element;
target.HydrateFallback = route.HydrateFallback;
target.action = route.action;
target.loader = route.loader;
target.handle = route.handle;
target.errorElement = route.errorElement;
Object.assign(target, {
element: layout.element,
HydrateFallback: layout.HydrateFallback,
action: layout.action,
loader: layout.loader,
handle: layout.handle,
errorElement: layout.errorElement,
});
return target;
}
function addRouteAsIndexRouteForTargetRoute(
export function addRouteAsIndexRouteForTargetRoute(
target: ExtendedRouteObject,
route: ExtendedRouteObject
page: ExtendedRouteObject
): ExtendedRouteObject {
target.children = target.children ?? [];
target.children = target.children || [];
target.children.push({
index: true,
element: route.element,
HydrateFallback: route.HydrateFallback,
action: route.action,
loader: route.loader,
handle: route.handle,
errorElement: route.errorElement,
element: page.element,
HydrateFallback: page.HydrateFallback,
action: page.action,
loader: page.loader,
handle: page.handle,
errorElement: page.errorElement,
});
return target;
}
function createRoute(args: {
segments: string[];
PageComponent: LazyExoticComponent<() => ReactElement>;
LoadingComponent?: LazyExoticComponent<() => ReactElement>;
PageComponent: LazyExoticComponent<() => ReactNode>;
LoadingComponent?: LazyExoticComponent<() => ReactNode>;
loader?: LoaderFunction;
action?: ActionFunction;
guard?: () => Promise<boolean>;
@@ -196,21 +253,24 @@ function createRoute(args: {
const [current, ...rest] = args.segments;
const [cleanPath, pageType] = current.split(separator);
const route: ExtendedRouteObject = { path: cleanPath };
if (pageType === 'page' || pageType === 'layout') {
route.element = <args.PageComponent />;
route.HydrateFallback =
args.LoadingComponent ?? (() => <div>Loading...</div>);
route.action = args.action;
route.loader = async (...props) => {
if (!(await args.guard?.())) {
throw new Response('Forbidden', {
status: 403,
statusText: 'Forbidden',
});
}
return args.loader?.(...props);
};
route.handle = { pageType: pageType as 'layout' | 'page' };
}
if (rest.length > 0) {
const nextSegment = rest[0].split(separator)[0];
if (nextSegment === 'update' || nextSegment === 'edit') {
return {
path: `${cleanPath}/${nextSegment}`,
@@ -221,20 +281,16 @@ function createRoute(args: {
handle: { pageType: pageType as 'layout' | 'page' },
};
}
const childRoute = createRoute({ ...args, segments: rest });
if (!route.children) {
route.children = [];
}
if (cleanPath.startsWith(':')) {
route.children.unshift(childRoute);
} else {
route.children.push(childRoute);
}
}
return route;
}
@@ -258,7 +314,6 @@ export function getRouteSegmentsFromFilePath(
if (segment.startsWith('[')) return getParamFromSegment(segment);
return segment;
});
return getRouteSegments(segments[0], segments, transformer);
}
@@ -306,7 +361,7 @@ export function addErrorElementToRoutes(
(_, prevSegment) => prevSegment
);
const ErrorBoundary = lazy(
importer as () => Promise<{ default: () => ReactElement }>
importer as () => Promise<{ default: () => ReactNode }>
);
setRoute(segments, routes, (route) => {
route.errorElement = <ErrorBoundary />;
@@ -325,7 +380,7 @@ export function add404PageToRoutesChildren(
(_, prevSegment) => prevSegment
);
const NotFound = lazy(
importer as () => Promise<{ default: () => ReactElement }>
importer as () => Promise<{ default: () => ReactNode }>
);
setRoute(segments, routes, (route) => {
if (route.children) {
@@ -340,9 +395,7 @@ export function add404PageToRoutesChildren(
action: tempRoute.action,
loader: tempRoute.loader,
});
route.children.push({ path: '*', element: <NotFound /> });
delete route.element;
delete route.action;
delete route.loader;
@@ -352,7 +405,7 @@ export function add404PageToRoutesChildren(
});
}
function set404NonPage(routes: RouteObject, notFoundElement: ReactElement) {
function set404NonPage(routes: RouteObject, notFoundElement: ReactNode) {
if (
routes.path &&
routes.children?.length &&
@@ -377,7 +430,6 @@ function setRoute(
segments.forEach((_segment, i) => {
const isLastSegment = i === segments.length - 1;
if (isLastSegment) return (temp = updater(temp));
if (!isLastSegment) {
const nextSegment = segments[i + 1];
const index = temp.children?.findIndex(