mirror of https://github.com/usememos/memos.git
fix: handle chunk load errors after redeployment with auto-reload (#5703)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e5b9392fcd
commit
bdd3554b89
|
|
@ -1,5 +1,6 @@
|
|||
import { AlertCircle, RefreshCw } from "lucide-react";
|
||||
import { Component, type ErrorInfo, type ReactNode } from "react";
|
||||
import { useRouteError } from "react-router-dom";
|
||||
import { Button } from "./ui/button";
|
||||
|
||||
interface Props {
|
||||
|
|
@ -68,3 +69,35 @@ export class ErrorBoundary extends Component<Props, State> {
|
|||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
// React Router errorElement for route-level errors (e.g., failed chunk loads after redeployment).
|
||||
export function ChunkLoadErrorFallback() {
|
||||
const error = useRouteError() as Error | undefined;
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen bg-background">
|
||||
<div className="max-w-md w-full p-6 space-y-4">
|
||||
<div className="flex items-center gap-3 text-destructive">
|
||||
<AlertCircle className="w-8 h-8" />
|
||||
<h1 className="text-2xl font-bold">Something went wrong</h1>
|
||||
</div>
|
||||
|
||||
<p className="text-foreground/70">
|
||||
An unexpected error occurred. This could be due to a network issue or an application update. Reloading usually fixes it.
|
||||
</p>
|
||||
|
||||
{error?.message && (
|
||||
<details className="bg-muted p-3 rounded-md text-sm">
|
||||
<summary className="cursor-pointer font-medium mb-2">Error details</summary>
|
||||
<pre className="whitespace-pre-wrap break-words text-xs text-foreground/60">{error.message}</pre>
|
||||
</details>
|
||||
)}
|
||||
|
||||
<Button onClick={() => window.location.reload()} className="w-full gap-2">
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
Reload Application
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,10 @@ const RootLayout = () => {
|
|||
|
||||
useEffect(() => {
|
||||
if (!currentUser) {
|
||||
if (pathname === ROUTES.ROOT && !memoRelatedSetting.disallowPublicVisibility) {
|
||||
if (memoRelatedSetting.disallowPublicVisibility) {
|
||||
// When public visibility is disallowed, always redirect unauth users to auth.
|
||||
redirectOnAuthFailure(true);
|
||||
} else if (pathname === ROUTES.ROOT) {
|
||||
navigateTo(ROUTES.EXPLORE);
|
||||
} else {
|
||||
redirectOnAuthFailure();
|
||||
|
|
|
|||
|
|
@ -1,23 +1,41 @@
|
|||
import { lazy } from "react";
|
||||
import { createBrowserRouter } from "react-router-dom";
|
||||
import App from "@/App";
|
||||
import { ChunkLoadErrorFallback } from "@/components/ErrorBoundary";
|
||||
import MainLayout from "@/layouts/MainLayout";
|
||||
import RootLayout from "@/layouts/RootLayout";
|
||||
import Home from "@/pages/Home";
|
||||
|
||||
const AdminSignIn = lazy(() => import("@/pages/AdminSignIn"));
|
||||
const Archived = lazy(() => import("@/pages/Archived"));
|
||||
const AuthCallback = lazy(() => import("@/pages/AuthCallback"));
|
||||
const Explore = lazy(() => import("@/pages/Explore"));
|
||||
const Inboxes = lazy(() => import("@/pages/Inboxes"));
|
||||
const MemoDetail = lazy(() => import("@/pages/MemoDetail"));
|
||||
const NotFound = lazy(() => import("@/pages/NotFound"));
|
||||
const PermissionDenied = lazy(() => import("@/pages/PermissionDenied"));
|
||||
const Attachments = lazy(() => import("@/pages/Attachments"));
|
||||
const Setting = lazy(() => import("@/pages/Setting"));
|
||||
const SignIn = lazy(() => import("@/pages/SignIn"));
|
||||
const SignUp = lazy(() => import("@/pages/SignUp"));
|
||||
const UserProfile = lazy(() => import("@/pages/UserProfile"));
|
||||
// Wrap lazy imports to auto-reload on chunk load failure (e.g., after redeployment).
|
||||
function lazyWithReload<T extends React.ComponentType>(factory: () => Promise<{ default: T }>) {
|
||||
return lazy(() =>
|
||||
factory().catch((error) => {
|
||||
const isChunkError =
|
||||
error?.message?.includes("Failed to fetch dynamically imported module") ||
|
||||
error?.message?.includes("Importing a module script failed");
|
||||
const reloadKey = "chunk-reload";
|
||||
if (isChunkError && !sessionStorage.getItem(reloadKey)) {
|
||||
sessionStorage.setItem(reloadKey, "1");
|
||||
window.location.reload();
|
||||
}
|
||||
throw error;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const AdminSignIn = lazyWithReload(() => import("@/pages/AdminSignIn"));
|
||||
const Archived = lazyWithReload(() => import("@/pages/Archived"));
|
||||
const AuthCallback = lazyWithReload(() => import("@/pages/AuthCallback"));
|
||||
const Explore = lazyWithReload(() => import("@/pages/Explore"));
|
||||
const Inboxes = lazyWithReload(() => import("@/pages/Inboxes"));
|
||||
const MemoDetail = lazyWithReload(() => import("@/pages/MemoDetail"));
|
||||
const NotFound = lazyWithReload(() => import("@/pages/NotFound"));
|
||||
const PermissionDenied = lazyWithReload(() => import("@/pages/PermissionDenied"));
|
||||
const Attachments = lazyWithReload(() => import("@/pages/Attachments"));
|
||||
const Setting = lazyWithReload(() => import("@/pages/Setting"));
|
||||
const SignIn = lazyWithReload(() => import("@/pages/SignIn"));
|
||||
const SignUp = lazyWithReload(() => import("@/pages/SignUp"));
|
||||
const UserProfile = lazyWithReload(() => import("@/pages/UserProfile"));
|
||||
|
||||
import { ROUTES } from "./routes";
|
||||
|
||||
|
|
@ -29,6 +47,7 @@ const router = createBrowserRouter([
|
|||
{
|
||||
path: "/",
|
||||
element: <App />,
|
||||
errorElement: <ChunkLoadErrorFallback />,
|
||||
children: [
|
||||
{
|
||||
path: Routes.AUTH,
|
||||
|
|
|
|||
|
|
@ -8,28 +8,23 @@ const PUBLIC_ROUTES = [
|
|||
"/memos/", // Individual memo detail pages (dynamic)
|
||||
] as const;
|
||||
|
||||
const PRIVATE_ROUTES = [ROUTES.ROOT, ROUTES.ATTACHMENTS, ROUTES.INBOX, ROUTES.ARCHIVED, ROUTES.SETTING] as const;
|
||||
|
||||
function isPublicRoute(path: string): boolean {
|
||||
return PUBLIC_ROUTES.some((route) => path.startsWith(route));
|
||||
}
|
||||
|
||||
function isPrivateRoute(path: string): boolean {
|
||||
return PRIVATE_ROUTES.includes(path as (typeof PRIVATE_ROUTES)[number]);
|
||||
}
|
||||
|
||||
export function redirectOnAuthFailure(): void {
|
||||
export function redirectOnAuthFailure(forceRedirect = false): void {
|
||||
const currentPath = window.location.pathname;
|
||||
|
||||
// Don't redirect if it's a public route
|
||||
if (isPublicRoute(currentPath)) {
|
||||
// Already on auth page, nothing to do.
|
||||
if (currentPath.startsWith(ROUTES.AUTH)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Always redirect to auth page on auth failure - the user's session expired
|
||||
// and they should re-authenticate rather than being sent to explore.
|
||||
if (isPrivateRoute(currentPath)) {
|
||||
clearAccessToken();
|
||||
window.location.replace(ROUTES.AUTH);
|
||||
// Don't redirect if it's a public route (unless forced, e.g. public visibility is disallowed).
|
||||
if (!forceRedirect && isPublicRoute(currentPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearAccessToken();
|
||||
window.location.replace(ROUTES.AUTH);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue