diff --git a/src/routes.tsx b/src/routes.tsx index 71633965..edca6864 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -7,31 +7,84 @@ export type AppRoute = Omit & { showGlobalSearch?: boolean; }; +const CHUNK_RELOAD_SESSION_KEY = 'gt:chunk-reloaded'; + +/** + * Wraps `React.lazy` with a one-shot recovery for stale-deploy chunk hashes. + * + * Vite emits hashed chunk filenames (e.g. `WatchlistPage-.js`). After a + * deploy, the previous entry chunk references stale hashes that no longer + * exist on the server, causing `import()` to fail with "Failed to fetch + * dynamically imported module". Reloading `index.html` always fetches the + * current entry chunk with up-to-date references. + * + * A sessionStorage flag prevents an infinite reload loop if the chunk is + * genuinely missing (real 404, not a stale-hash 404). + */ +const lazyWithReload = >( + factory: () => Promise<{ default: T }>, +): React.LazyExoticComponent => + React.lazy(() => + factory() + .then((mod) => { + // Successful chunk load — reset the recovery flag so any *future* + // stale-deploy event also gets one fresh reload attempt. + if (typeof window !== 'undefined') { + window.sessionStorage.removeItem(CHUNK_RELOAD_SESSION_KEY); + } + return mod; + }) + .catch((err: Error) => { + const message = String(err?.message ?? err); + const isChunkLoadError = + /Failed to fetch dynamically imported module|Importing a module script failed|ChunkLoadError/i.test( + message, + ); + if ( + isChunkLoadError && + typeof window !== 'undefined' && + !window.sessionStorage.getItem(CHUNK_RELOAD_SESSION_KEY) + ) { + window.sessionStorage.setItem(CHUNK_RELOAD_SESSION_KEY, '1'); + window.location.reload(); + // Resolve with an empty component while the reload is pending. + return { default: (() => null) as unknown as T }; + } + throw err; + }), + ); + // main menu pages -const HomePage = React.lazy(() => import('./pages/HomePage')); +const HomePage = lazyWithReload(() => import('./pages/HomePage')); // AboutPage and FAQPage deleted — redirects inline below -const DashboardPage = React.lazy( +const DashboardPage = lazyWithReload( () => import('./pages/dashboard/DashboardPage'), ); -const IssuesPage = React.lazy(() => import('./pages/IssuesPage')); -const SearchPage = React.lazy(() => import('./pages/search/SearchPage')); -const IssueDetailsPage = React.lazy(() => import('./pages/IssueDetailsPage')); -const TopMinersPage = React.lazy(() => import('./pages/TopMinersPage')); -const RepositoriesPage = React.lazy(() => import('./pages/RepositoriesPage')); -const MinerDetailsPage = React.lazy(() => import('./pages/MinerDetailsPage')); -const RepositoryDetailsPage = React.lazy( +const IssuesPage = lazyWithReload(() => import('./pages/IssuesPage')); +const SearchPage = lazyWithReload(() => import('./pages/search/SearchPage')); +const IssueDetailsPage = lazyWithReload( + () => import('./pages/IssueDetailsPage'), +); +const TopMinersPage = lazyWithReload(() => import('./pages/TopMinersPage')); +const RepositoriesPage = lazyWithReload( + () => import('./pages/RepositoriesPage'), +); +const MinerDetailsPage = lazyWithReload( + () => import('./pages/MinerDetailsPage'), +); +const RepositoryDetailsPage = lazyWithReload( () => import('./pages/RepositoryDetailsPage'), ); -const PRDetailsPage = React.lazy(() => import('./pages/PRDetailsPage')); +const PRDetailsPage = lazyWithReload(() => import('./pages/PRDetailsPage')); -const OnboardPage = React.lazy(() => import('./pages/OnboardPage')); -const WatchlistPage = React.lazy(() => import('./pages/WatchlistPage')); -const RepositoryRegistrationPage = React.lazy( +const OnboardPage = lazyWithReload(() => import('./pages/OnboardPage')); +const WatchlistPage = lazyWithReload(() => import('./pages/WatchlistPage')); +const RepositoryRegistrationPage = lazyWithReload( () => import('./pages/RepositoryRegistrationPage'), ); // 404 page -const NotFoundPage = React.lazy(() => import('./pages/NotFoundPage')); +const NotFoundPage = lazyWithReload(() => import('./pages/NotFoundPage')); const routesArray: AppRoute[] = [ { name: 'home', path: '/', element: },