diff --git a/packages/console/package.json b/packages/console/package.json index aee57fe6cc0..4e24b8687ba 100644 --- a/packages/console/package.json +++ b/packages/console/package.json @@ -107,6 +107,7 @@ "react-modal": "^3.15.1", "react-paginate": "^8.1.3", "react-router-dom": "^6.25.1", + "react-safe-lazy": "^0.1.0", "react-syntax-highlighter": "^15.5.0", "react-timer-hook": "^3.0.5", "recharts": "^2.1.13", diff --git a/packages/console/src/containers/ConsoleContent/index.tsx b/packages/console/src/containers/ConsoleContent/index.tsx index 3ad47568e5f..d063da006ec 100644 --- a/packages/console/src/containers/ConsoleContent/index.tsx +++ b/packages/console/src/containers/ConsoleContent/index.tsx @@ -1,5 +1,6 @@ import { Suspense } from 'react'; import { useOutletContext, useRoutes } from 'react-router-dom'; +import { safeLazy } from 'react-safe-lazy'; import { isDevFeaturesEnabled } from '@/consts/env'; import OverlayScrollbar from '@/ds-components/OverlayScrollbar'; @@ -7,7 +8,6 @@ import { Daisy } from '@/ds-components/Spinner'; import Tag from '@/ds-components/Tag'; import { useConsoleRoutes } from '@/hooks/use-console-routes'; import { usePlausiblePageview } from '@/hooks/use-plausible-pageview'; -import safeLazy from '@/utils/lazy'; import type { AppContentOutletContext } from '../AppContent/types'; diff --git a/packages/console/src/containers/ConsoleRoutes/index.tsx b/packages/console/src/containers/ConsoleRoutes/index.tsx index e1342451b88..f3834d514d3 100644 --- a/packages/console/src/containers/ConsoleRoutes/index.tsx +++ b/packages/console/src/containers/ConsoleRoutes/index.tsx @@ -1,10 +1,11 @@ import { ossConsolePath } from '@logto/schemas'; import { Suspense } from 'react'; import { Navigate, Outlet, Route, Routes } from 'react-router-dom'; +import { safeLazy } from 'react-safe-lazy'; import { SWRConfig } from 'swr'; import AppLoading from '@/components/AppLoading'; -import { isCloud } from '@/consts/env'; +import { isCloud, isDevFeaturesEnabled } from '@/consts/env'; import AppBoundary from '@/containers/AppBoundary'; import AppContent, { RedirectToFirstItem } from '@/containers/AppContent'; import ConsoleContent from '@/containers/ConsoleContent'; @@ -14,7 +15,6 @@ import { GlobalRoute } from '@/contexts/TenantsProvider'; import useSwrOptions from '@/hooks/use-swr-options'; import Callback from '@/pages/Callback'; import CheckoutSuccessCallback from '@/pages/CheckoutSuccessCallback'; -import safeLazy from '@/utils/lazy'; import { dropLeadingSlash } from '@/utils/url'; import { __Internal__ImportError } from './internal'; @@ -47,7 +47,9 @@ export function ConsoleRoutes() { }> } /> } /> - } /> + {isDevFeaturesEnabled && ( + } /> + )} }> } /> }> diff --git a/packages/console/src/containers/ConsoleRoutes/internal.ts b/packages/console/src/containers/ConsoleRoutes/internal.ts index 4a9b7319628..efa055872e4 100644 --- a/packages/console/src/containers/ConsoleRoutes/internal.ts +++ b/packages/console/src/containers/ConsoleRoutes/internal.ts @@ -1,5 +1,9 @@ -import safeLazy from '@/utils/lazy'; +import { safeLazy } from 'react-safe-lazy'; +/** + * An internal module that is used to test the lazy loading failure in the console. Normally, this + * module should not involve any production code. + */ export const __Internal__ImportError = safeLazy(async () => { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const module = await import( diff --git a/packages/console/src/hooks/use-console-routes/index.tsx b/packages/console/src/hooks/use-console-routes/index.tsx index 83ae9f9b4a8..85e8b4d190a 100644 --- a/packages/console/src/hooks/use-console-routes/index.tsx +++ b/packages/console/src/hooks/use-console-routes/index.tsx @@ -1,10 +1,10 @@ import { condArray } from '@silverhand/essentials'; import { useMemo } from 'react'; import { type RouteObject } from 'react-router-dom'; +import { safeLazy } from 'react-safe-lazy'; import { isCloud } from '@/consts/env'; import NotFound from '@/pages/NotFound'; -import safeLazy from '@/utils/lazy'; import { apiResources } from './routes/api-resources'; import { applications } from './routes/applications'; diff --git a/packages/console/src/hooks/use-console-routes/routes/api-resources.tsx b/packages/console/src/hooks/use-console-routes/routes/api-resources.tsx index f51b5a58d3f..8944f309cc2 100644 --- a/packages/console/src/hooks/use-console-routes/routes/api-resources.tsx +++ b/packages/console/src/hooks/use-console-routes/routes/api-resources.tsx @@ -1,7 +1,7 @@ import { Navigate, type RouteObject } from 'react-router-dom'; +import { safeLazy } from 'react-safe-lazy'; import { ApiResourceDetailsTabs } from '@/consts'; -import safeLazy from '@/utils/lazy'; const ApiResources = safeLazy(async () => import('@/pages/ApiResources')); const ApiResourceDetails = safeLazy(async () => import('@/pages/ApiResourceDetails')); diff --git a/packages/console/src/hooks/use-console-routes/routes/applications.tsx b/packages/console/src/hooks/use-console-routes/routes/applications.tsx index 59e9dbe1691..bdb27c803d7 100644 --- a/packages/console/src/hooks/use-console-routes/routes/applications.tsx +++ b/packages/console/src/hooks/use-console-routes/routes/applications.tsx @@ -1,7 +1,7 @@ import { Navigate, type RouteObject } from 'react-router-dom'; +import { safeLazy } from 'react-safe-lazy'; import { ApplicationDetailsTabs } from '@/consts'; -import safeLazy from '@/utils/lazy'; const Applications = safeLazy(async () => import('@/pages/Applications')); const ApplicationDetails = safeLazy(async () => import('@/pages/ApplicationDetails')); diff --git a/packages/console/src/hooks/use-console-routes/routes/audit-logs.tsx b/packages/console/src/hooks/use-console-routes/routes/audit-logs.tsx index 32b5604fa85..626c91527e6 100644 --- a/packages/console/src/hooks/use-console-routes/routes/audit-logs.tsx +++ b/packages/console/src/hooks/use-console-routes/routes/audit-logs.tsx @@ -1,6 +1,5 @@ import { type RouteObject } from 'react-router-dom'; - -import safeLazy from '@/utils/lazy'; +import { safeLazy } from 'react-safe-lazy'; const AuditLogs = safeLazy(async () => import('@/pages/AuditLogs')); const AuditLogDetails = safeLazy(async () => import('@/pages/AuditLogDetails')); diff --git a/packages/console/src/hooks/use-console-routes/routes/connectors.tsx b/packages/console/src/hooks/use-console-routes/routes/connectors.tsx index 70526b338bc..f79079393c5 100644 --- a/packages/console/src/hooks/use-console-routes/routes/connectors.tsx +++ b/packages/console/src/hooks/use-console-routes/routes/connectors.tsx @@ -1,7 +1,7 @@ import { Navigate } from 'react-router-dom'; +import { safeLazy } from 'react-safe-lazy'; import { ConnectorsTabs } from '@/consts'; -import safeLazy from '@/utils/lazy'; const Connectors = safeLazy(async () => import('@/pages/Connectors')); const ConnectorDetails = safeLazy(async () => import('@/pages/ConnectorDetails')); diff --git a/packages/console/src/hooks/use-console-routes/routes/customize-jwt.tsx b/packages/console/src/hooks/use-console-routes/routes/customize-jwt.tsx index 7ccd3a34a2f..24b3fe44cc2 100644 --- a/packages/console/src/hooks/use-console-routes/routes/customize-jwt.tsx +++ b/packages/console/src/hooks/use-console-routes/routes/customize-jwt.tsx @@ -1,6 +1,5 @@ import { type RouteObject } from 'react-router-dom'; - -import safeLazy from '@/utils/lazy'; +import { safeLazy } from 'react-safe-lazy'; const CustomizeJwt = safeLazy(async () => import('@/pages/CustomizeJwt')); const CustomizeJwtDetails = safeLazy(async () => import('@/pages/CustomizeJwtDetails')); diff --git a/packages/console/src/hooks/use-console-routes/routes/enterprise-sso.tsx b/packages/console/src/hooks/use-console-routes/routes/enterprise-sso.tsx index d52297a2c9a..a70e61975a9 100644 --- a/packages/console/src/hooks/use-console-routes/routes/enterprise-sso.tsx +++ b/packages/console/src/hooks/use-console-routes/routes/enterprise-sso.tsx @@ -1,7 +1,7 @@ import { Navigate, type RouteObject } from 'react-router-dom'; +import { safeLazy } from 'react-safe-lazy'; import { EnterpriseSsoDetailsTabs } from '@/consts/page-tabs'; -import safeLazy from '@/utils/lazy'; const EnterpriseSso = safeLazy(async () => import('@/pages/EnterpriseSso')); const EnterpriseSsoDetails = safeLazy(async () => import('@/pages/EnterpriseSsoDetails')); diff --git a/packages/console/src/hooks/use-console-routes/routes/mfa.tsx b/packages/console/src/hooks/use-console-routes/routes/mfa.tsx index 5335997b38a..b35ccdb7fe4 100644 --- a/packages/console/src/hooks/use-console-routes/routes/mfa.tsx +++ b/packages/console/src/hooks/use-console-routes/routes/mfa.tsx @@ -1,6 +1,5 @@ import { type RouteObject } from 'react-router-dom'; - -import safeLazy from '@/utils/lazy'; +import { safeLazy } from 'react-safe-lazy'; const Mfa = safeLazy(async () => import('@/pages/Mfa')); diff --git a/packages/console/src/hooks/use-console-routes/routes/organization-template.tsx b/packages/console/src/hooks/use-console-routes/routes/organization-template.tsx index d94f9b6d18d..6242c363c68 100644 --- a/packages/console/src/hooks/use-console-routes/routes/organization-template.tsx +++ b/packages/console/src/hooks/use-console-routes/routes/organization-template.tsx @@ -1,7 +1,7 @@ import { Navigate, type RouteObject } from 'react-router-dom'; +import { safeLazy } from 'react-safe-lazy'; import { OrganizationRoleDetailsTabs, OrganizationTemplateTabs } from '@/consts'; -import safeLazy from '@/utils/lazy'; const OrganizationTemplate = safeLazy(async () => import('@/pages/OrganizationTemplate')); const OrganizationRoles = safeLazy( diff --git a/packages/console/src/hooks/use-console-routes/routes/organizations.tsx b/packages/console/src/hooks/use-console-routes/routes/organizations.tsx index d8b119ea8cf..20f73d6c90b 100644 --- a/packages/console/src/hooks/use-console-routes/routes/organizations.tsx +++ b/packages/console/src/hooks/use-console-routes/routes/organizations.tsx @@ -1,8 +1,8 @@ import { condArray } from '@silverhand/essentials'; import { Navigate, type RouteObject } from 'react-router-dom'; +import { safeLazy } from 'react-safe-lazy'; import { OrganizationDetailsTabs } from '@/pages/OrganizationDetails/types'; -import safeLazy from '@/utils/lazy'; const Organizations = safeLazy(async () => import('@/pages/Organizations')); const OrganizationDetails = safeLazy(async () => import('@/pages/OrganizationDetails')); diff --git a/packages/console/src/hooks/use-console-routes/routes/profile.tsx b/packages/console/src/hooks/use-console-routes/routes/profile.tsx index 9c22446593a..17cdb177649 100644 --- a/packages/console/src/hooks/use-console-routes/routes/profile.tsx +++ b/packages/console/src/hooks/use-console-routes/routes/profile.tsx @@ -1,6 +1,5 @@ import { type RouteObject } from 'react-router-dom'; - -import safeLazy from '@/utils/lazy'; +import { safeLazy } from 'react-safe-lazy'; const ChangePasswordModal = safeLazy( async () => import('@/pages/Profile/containers/ChangePasswordModal') diff --git a/packages/console/src/hooks/use-console-routes/routes/roles.tsx b/packages/console/src/hooks/use-console-routes/routes/roles.tsx index de2fba2be55..73a1269eb88 100644 --- a/packages/console/src/hooks/use-console-routes/routes/roles.tsx +++ b/packages/console/src/hooks/use-console-routes/routes/roles.tsx @@ -1,7 +1,7 @@ import { Navigate, type RouteObject } from 'react-router-dom'; +import { safeLazy } from 'react-safe-lazy'; import { RoleDetailsTabs } from '@/consts/page-tabs'; -import safeLazy from '@/utils/lazy'; const Roles = safeLazy(async () => import('@/pages/Roles')); const RoleDetails = safeLazy(async () => import('@/pages/RoleDetails')); diff --git a/packages/console/src/hooks/use-console-routes/routes/sign-in-experience.tsx b/packages/console/src/hooks/use-console-routes/routes/sign-in-experience.tsx index 221b436a7f7..f0c705ff9a2 100644 --- a/packages/console/src/hooks/use-console-routes/routes/sign-in-experience.tsx +++ b/packages/console/src/hooks/use-console-routes/routes/sign-in-experience.tsx @@ -1,7 +1,7 @@ import { Navigate, type RouteObject } from 'react-router-dom'; +import { safeLazy } from 'react-safe-lazy'; import { SignInExperienceTab } from '@/pages/SignInExperience/types'; -import safeLazy from '@/utils/lazy'; const SignInExperience = safeLazy(async () => import('@/pages/SignInExperience')); diff --git a/packages/console/src/hooks/use-console-routes/routes/tenant-settings.tsx b/packages/console/src/hooks/use-console-routes/routes/tenant-settings.tsx index ed158d47d0e..767d9c33595 100644 --- a/packages/console/src/hooks/use-console-routes/routes/tenant-settings.tsx +++ b/packages/console/src/hooks/use-console-routes/routes/tenant-settings.tsx @@ -1,12 +1,12 @@ import { condArray } from '@silverhand/essentials'; import { useContext, useMemo } from 'react'; import { Navigate, type RouteObject } from 'react-router-dom'; +import { safeLazy } from 'react-safe-lazy'; import { TenantSettingsTabs } from '@/consts'; import { TenantsContext } from '@/contexts/TenantsProvider'; import useCurrentTenantScopes from '@/hooks/use-current-tenant-scopes'; import NotFound from '@/pages/NotFound'; -import safeLazy from '@/utils/lazy'; const TenantSettings = safeLazy(async () => import('@/pages/TenantSettings')); const TenantBasicSettings = safeLazy( diff --git a/packages/console/src/hooks/use-console-routes/routes/users.tsx b/packages/console/src/hooks/use-console-routes/routes/users.tsx index 18dc12f4847..0bad4fc5237 100644 --- a/packages/console/src/hooks/use-console-routes/routes/users.tsx +++ b/packages/console/src/hooks/use-console-routes/routes/users.tsx @@ -1,7 +1,7 @@ import { Navigate, type RouteObject } from 'react-router-dom'; +import { safeLazy } from 'react-safe-lazy'; import { UserDetailsTabs } from '@/consts/page-tabs'; -import safeLazy from '@/utils/lazy'; const AuditLogDetails = safeLazy(async () => import('@/pages/AuditLogDetails')); const UserDetails = safeLazy(async () => import('@/pages/UserDetails')); diff --git a/packages/console/src/hooks/use-console-routes/routes/webhooks.tsx b/packages/console/src/hooks/use-console-routes/routes/webhooks.tsx index c0ad8542705..2cc47ee4687 100644 --- a/packages/console/src/hooks/use-console-routes/routes/webhooks.tsx +++ b/packages/console/src/hooks/use-console-routes/routes/webhooks.tsx @@ -1,7 +1,7 @@ import { Navigate, type RouteObject } from 'react-router-dom'; +import { safeLazy } from 'react-safe-lazy'; import { WebhookDetailsTabs } from '@/consts'; -import safeLazy from '@/utils/lazy'; const WebhookDetails = safeLazy(async () => import('@/pages/WebhookDetails')); const AuditLogDetails = safeLazy(async () => import('@/pages/AuditLogDetails')); diff --git a/packages/console/src/utils/lazy.ts b/packages/console/src/utils/lazy.ts deleted file mode 100644 index e2ff1d5ebd0..00000000000 --- a/packages/console/src/utils/lazy.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { type ComponentType, lazy } from 'react'; - -class ForceReloadStorage { - storageKey = 'forceReloadedFunctionNames'; - - getNames(): Set { - const stored = sessionStorage.getItem(this.storageKey); - - try { - const parsed: unknown = stored ? JSON.parse(stored) : []; - return new Set( - Array.isArray(parsed) ? parsed.filter((value) => typeof value === 'string') : undefined - ); - } catch (error) { - console.error(error); - return new Set(); - } - } - - addName(functionName: string) { - const stored = this.getNames(); - stored.add(functionName); - sessionStorage.setItem(this.storageKey, JSON.stringify(Array.from(stored))); - } - - removeName(functionName: string) { - const stored = this.getNames(); - stored.delete(functionName); - sessionStorage.setItem(this.storageKey, JSON.stringify(Array.from(stored))); - } -} - -const reloadStorage = new ForceReloadStorage(); - -/** - * A wrapper around React's `lazy` function that reloads the page if the lazy-loaded component - * fails to load. If the component fails to load twice, it will not attempt to reload the page - * again. - */ -const safeLazy = (importFunction: () => Promise<{ default: ComponentType }>) => - lazy(async () => { - const functionString = importFunction.toString(); - - try { - const component = await importFunction(); - reloadStorage.removeName(functionString); - return component; - } catch (error) { - console.error(error); - - if (!reloadStorage.getNames().has(functionString)) { - reloadStorage.addName(functionString); - window.location.reload(); - return { default: () => null }; - } - - throw error; - } - }); - -export default safeLazy; diff --git a/packages/integration-tests/src/tests/console/error-handling.test.ts b/packages/integration-tests/src/tests/console/error-handling.test.ts index dd5a84572d2..a8614686ce4 100644 --- a/packages/integration-tests/src/tests/console/error-handling.test.ts +++ b/packages/integration-tests/src/tests/console/error-handling.test.ts @@ -2,11 +2,12 @@ import { appendPath } from '@silverhand/essentials'; import ExpectConsole from '#src/ui-helpers/expect-console.js'; import { Trace } from '#src/ui-helpers/trace.js'; +import { devFeatureTest } from '#src/utils.js'; describe('error handling', () => { const trace = new Trace(); - it('should handle dynamic import errors', async () => { + devFeatureTest.it('should handle dynamic import errors', async () => { const expectConsole = new ExpectConsole(await browser.newPage()); const path = appendPath(expectConsole.options.endpoint, 'console/__internal__/import-error'); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 86b76b1e100..d46ec30669d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3100,6 +3100,9 @@ importers: react-router-dom: specifier: ^6.25.1 version: 6.25.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-safe-lazy: + specifier: ^0.1.0 + version: 0.1.0(react@18.3.1) react-syntax-highlighter: specifier: ^15.5.0 version: 15.5.0(react@18.3.1) @@ -11700,6 +11703,11 @@ packages: peerDependencies: react: '>=16.8' + react-safe-lazy@0.1.0: + resolution: {integrity: sha512-CZSaQHlNVG8OuSRaLkBGe5cP+qjZtW+fJA/PRNTyCIVkH3F7GQO0aBu+jIrm2PFrTVs4xjSvuDFVyzlX6OL0oQ==} + peerDependencies: + react: ^18.0.0 + react-side-effect@2.1.2: resolution: {integrity: sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==} peerDependencies: @@ -23042,6 +23050,10 @@ snapshots: '@remix-run/router': 1.18.0 react: 18.3.1 + react-safe-lazy@0.1.0(react@18.3.1): + dependencies: + react: 18.3.1 + react-side-effect@2.1.2(react@18.3.1): dependencies: react: 18.3.1