Files
kandev/apps/web/components/plugins/plugin-error-boundary.tsx
2026-08-05 12:53:55 +02:00

57 lines
1.8 KiB
TypeScript

"use client";
import { Component, type ErrorInfo, type ReactNode } from "react";
import { useTranslation } from "react-i18next";
type PluginErrorBoundaryProps = {
/**
* Identifies what threw in the console log, e.g. `slot "task-sidebar"` or
* `route "/plugins/hello"`. A developer diagnostic that never reaches the
* DOM — `context` is in the guard's `jsx-attributes.exclude` for that reason.
*/
context: string;
/** Rendered in place of `children` after a throw. Defaults to nothing (silent). */
fallback?: ReactNode;
children: ReactNode;
};
type PluginErrorBoundaryState = {
error: Error | null;
};
/**
* Isolates plugin-owned React output (a slot component, a top-level plugin
* route, or a plugin settings route) so a throw anywhere in one plugin's
* render can't tear down the host shell or other plugins — plugin code runs
* in-process (see PLUGIN-API.md security posture).
*/
export class PluginErrorBoundary extends Component<
PluginErrorBoundaryProps,
PluginErrorBoundaryState
> {
state: PluginErrorBoundaryState = { error: null };
static getDerivedStateFromError(error: Error): PluginErrorBoundaryState {
return { error };
}
componentDidCatch(error: Error, info: ErrorInfo): void {
console.error(`[plugins] ${this.props.context} threw`, error, info.componentStack);
}
render(): ReactNode {
if (this.state.error) return this.props.fallback ?? null;
return this.props.children;
}
}
/** Shared fallback for a plugin-owned top-level or settings route that threw during render. */
export function PluginRouteFallback() {
const { t } = useTranslation();
return (
<div className="flex flex-1 items-center justify-center p-8 text-sm text-muted-foreground">
{t("plugins:pluginPageFailedToLoad")}
</div>
);
}