85 lines
2.6 KiB
TypeScript
85 lines
2.6 KiB
TypeScript
|
|
import React, { Suspense, lazy, Component, ReactNode } from 'react';
|
||
|
|
import { Loader2 } from 'lucide-react';
|
||
|
|
|
||
|
|
// ── Error Boundary ──────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
interface PluginErrorBoundaryProps {
|
||
|
|
children: ReactNode;
|
||
|
|
pluginName: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface PluginErrorBoundaryState {
|
||
|
|
hasError: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
class PluginErrorBoundary extends Component<PluginErrorBoundaryProps, PluginErrorBoundaryState> {
|
||
|
|
state = { hasError: false };
|
||
|
|
|
||
|
|
static getDerivedStateFromError(): PluginErrorBoundaryState {
|
||
|
|
return { hasError: true };
|
||
|
|
}
|
||
|
|
|
||
|
|
render() {
|
||
|
|
if (this.state.hasError) {
|
||
|
|
return (
|
||
|
|
<div className="p-4 text-sm text-red-600" role="alert">
|
||
|
|
Failed to load plugin: {this.props.pluginName}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
return this.props.children;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Lazy Component Cache ────────────────────────────────────────────────
|
||
|
|
|
||
|
|
const componentCache = new Map<string, React.LazyExoticComponent<React.ComponentType<any>>>();
|
||
|
|
|
||
|
|
function getLazyComponent(componentPath: string): React.LazyExoticComponent<React.ComponentType<any>> {
|
||
|
|
if (componentCache.has(componentPath)) {
|
||
|
|
return componentCache.get(componentPath)!;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Convert @/pages/Mail to ../pages/Mail for dynamic import
|
||
|
|
const importPath = componentPath.replace(/^@\//, '../');
|
||
|
|
|
||
|
|
const LazyComp = lazy(() =>
|
||
|
|
import(/* @vite-ignore */ importPath).then((m) => ({
|
||
|
|
default: m.default || m[Object.keys(m)[0]],
|
||
|
|
}))
|
||
|
|
);
|
||
|
|
|
||
|
|
componentCache.set(componentPath, LazyComp);
|
||
|
|
return LazyComp;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── PluginPage Component ───────────────────────────────────────────────
|
||
|
|
|
||
|
|
interface PluginPageProps {
|
||
|
|
component: string;
|
||
|
|
pluginName: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* PluginPage — dynamically loads a plugin page component using React.lazy().
|
||
|
|
* Wraps the lazy component in Suspense with a centered spinner fallback
|
||
|
|
* and an ErrorBoundary that shows a fallback message on failure.
|
||
|
|
*/
|
||
|
|
export function PluginPage({ component, pluginName }: PluginPageProps) {
|
||
|
|
const LazyComp = getLazyComponent(component);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<PluginErrorBoundary pluginName={pluginName}>
|
||
|
|
<Suspense
|
||
|
|
fallback={
|
||
|
|
<div className="flex items-center justify-center min-h-[50vh]">
|
||
|
|
<Loader2 className="animate-spin h-8 w-8 text-primary-500" />
|
||
|
|
</div>
|
||
|
|
}
|
||
|
|
>
|
||
|
|
<LazyComp />
|
||
|
|
</Suspense>
|
||
|
|
</PluginErrorBoundary>
|
||
|
|
);
|
||
|
|
}
|