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