Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion web/src/app/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Layout } from './layout'
import { getCurrentUser } from '@/api/client'
import { RoleGuard } from '@/shared/components/role-guard'
import { createRequireAuth } from '@/shared/lib/auth-route'
import { clearDynamicImportReloadGuard, recoverFromDynamicImportError } from '@/shared/lib/dynamic-import-recovery'
import { normalizeSearchQuery } from '@/shared/lib/search-query'

/**
Expand All @@ -25,7 +26,12 @@ function createLazyRouteComponent<TModule extends Record<string, unknown>>(
// Lazy route modules are wrapped in a uniform suspense fallback so route transitions behave
// consistently across public and dashboard pages.
const LazyComponent = lazy(async () => {
const module = await importer()
const module = await importer().catch((error) => {
if (recoverFromDynamicImportError(error)) {
return new Promise<never>(() => {})
}
throw error
})
return { default: module[exportName] as ComponentType<Record<string, unknown>> }
})

Expand Down Expand Up @@ -477,6 +483,10 @@ export const router = createRouter({
defaultNotFoundComponent: DefaultNotFound,
})

router.subscribe('onResolved', () => {
clearDynamicImportReloadGuard()
})

declare module '@tanstack/react-router' {
interface Register {
router: typeof router
Expand Down
38 changes: 38 additions & 0 deletions web/src/shared/lib/dynamic-import-recovery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const RELOAD_GUARD_KEY = 'skillhub:dynamic-import-reload'

function resolveErrorMessage(error: unknown): string {
if (error instanceof Error) {
return error.message
}
return String(error ?? '')
}

export function isDynamicImportFetchError(error: unknown): boolean {
const message = resolveErrorMessage(error)
return message.includes('Failed to fetch dynamically imported module')
|| message.includes('error loading dynamically imported module')
|| message.includes('Importing a module script failed')
|| message.includes('ChunkLoadError')
}

export function recoverFromDynamicImportError(error: unknown): boolean {
if (typeof window === 'undefined' || !isDynamicImportFetchError(error)) {
return false
}

if (window.sessionStorage.getItem(RELOAD_GUARD_KEY) === '1') {
window.sessionStorage.removeItem(RELOAD_GUARD_KEY)
return false
}

window.sessionStorage.setItem(RELOAD_GUARD_KEY, '1')
window.location.reload()
return true
}

export function clearDynamicImportReloadGuard(): void {
if (typeof window === 'undefined') {
return
}
window.sessionStorage.removeItem(RELOAD_GUARD_KEY)
}