diff --git a/packages/console/.eslintrc.cjs b/packages/console/.eslintrc.cjs index a350201e7..0997dc740 100644 --- a/packages/console/.eslintrc.cjs +++ b/packages/console/.eslintrc.cjs @@ -15,6 +15,7 @@ module.exports = { unnamedComponents: 'arrow-function', }, ], + 'react/jsx-pascal-case': ['error', { ignore: ['__Internal__*'] }], 'import/no-unused-modules': [ 'error', { @@ -30,6 +31,8 @@ module.exports = { '**/assets/docs/guides/*/index.ts', '**/assets/docs/guides/*/components/**/*.tsx', '**/mdx-components*/*/index.tsx', + '*.config.js', + '*.config.ts', ], rules: { 'import/no-unused-modules': 'off', @@ -49,12 +52,6 @@ module.exports = { ], }, }, - { - files: ['*.config.js', '*.config.ts', '*.d.ts'], - rules: { - 'import/no-unused-modules': 'off', - }, - }, { files: ['*.d.ts'], rules: { diff --git a/packages/console/package.json b/packages/console/package.json index aee57fe6c..4e24b8687 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 49ff99674..d063da006 100644 --- a/packages/console/src/containers/ConsoleContent/index.tsx +++ b/packages/console/src/containers/ConsoleContent/index.tsx @@ -1,5 +1,6 @@ -import { lazy, Suspense } from 'react'; +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'; @@ -14,7 +15,7 @@ import { Skeleton } from './Sidebar'; import useTenantScopeListener from './hooks'; import styles from './index.module.scss'; -const Sidebar = lazy(async () => import('./Sidebar')); +const Sidebar = safeLazy(async () => import('./Sidebar')); function ConsoleContent() { const { scrollableContent } = useOutletContext(); diff --git a/packages/console/src/containers/ConsoleRoutes/index.tsx b/packages/console/src/containers/ConsoleRoutes/index.tsx index 4e4e2369f..f3834d514 100644 --- a/packages/console/src/containers/ConsoleRoutes/index.tsx +++ b/packages/console/src/containers/ConsoleRoutes/index.tsx @@ -1,8 +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 { isCloud } from '@/consts/env'; +import AppLoading from '@/components/AppLoading'; +import { isCloud, isDevFeaturesEnabled } from '@/consts/env'; import AppBoundary from '@/containers/AppBoundary'; import AppContent, { RedirectToFirstItem } from '@/containers/AppContent'; import ConsoleContent from '@/containers/ConsoleContent'; @@ -12,10 +15,13 @@ import { GlobalRoute } from '@/contexts/TenantsProvider'; import useSwrOptions from '@/hooks/use-swr-options'; import Callback from '@/pages/Callback'; import CheckoutSuccessCallback from '@/pages/CheckoutSuccessCallback'; -import Profile from '@/pages/Profile'; -import Welcome from '@/pages/Welcome'; import { dropLeadingSlash } from '@/utils/url'; +import { __Internal__ImportError } from './internal'; + +const Welcome = safeLazy(async () => import('@/pages/Welcome')); +const Profile = safeLazy(async () => import('@/pages/Profile')); + function Layout() { const swrOptions = useSwrOptions(); @@ -30,32 +36,37 @@ function Layout() { export function ConsoleRoutes() { return ( - - {/** - * OSS doesn't have a tenant concept nor root path handling component, but it may - * navigate to the root path in frontend. In this case, we redirect it to the OSS - * console path to trigger the console routes. - */} - {!isCloud && } />} - }> - } /> - } /> - }> - } /> - }> - {isCloud && ( - } - /> - )} - }> - } /> - } /> + }> + + {/** + * OSS doesn't have a tenant concept nor root path handling component, but it may + * navigate to the root path in frontend. In this case, we redirect it to the OSS + * console path to trigger the console routes. + */} + {!isCloud && } />} + }> + } /> + } /> + {isDevFeaturesEnabled && ( + } /> + )} + }> + } /> + }> + {isCloud && ( + } + /> + )} + }> + } /> + } /> + - - + + ); } diff --git a/packages/console/src/containers/ConsoleRoutes/internal.ts b/packages/console/src/containers/ConsoleRoutes/internal.ts new file mode 100644 index 000000000..efa055872 --- /dev/null +++ b/packages/console/src/containers/ConsoleRoutes/internal.ts @@ -0,0 +1,14 @@ +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( + /* @vite-ignore */ `${window.location.origin}/some-non-existing-path` + ); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return module; +}); diff --git a/packages/console/src/hooks/use-console-routes/index.tsx b/packages/console/src/hooks/use-console-routes/index.tsx index 4b78bbc2d..85e8b4d19 100644 --- a/packages/console/src/hooks/use-console-routes/index.tsx +++ b/packages/console/src/hooks/use-console-routes/index.tsx @@ -1,6 +1,7 @@ import { condArray } from '@silverhand/essentials'; -import { lazy, useMemo } from 'react'; +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'; @@ -20,9 +21,9 @@ import { useTenantSettings } from './routes/tenant-settings'; import { users } from './routes/users'; import { webhooks } from './routes/webhooks'; -const Dashboard = lazy(async () => import('@/pages/Dashboard')); -const GetStarted = lazy(async () => import('@/pages/GetStarted')); -const SigningKeys = lazy(async () => import('@/pages/SigningKeys')); +const Dashboard = safeLazy(async () => import('@/pages/Dashboard')); +const GetStarted = safeLazy(async () => import('@/pages/GetStarted')); +const SigningKeys = safeLazy(async () => import('@/pages/SigningKeys')); export const useConsoleRoutes = () => { const tenantSettings = useTenantSettings(); 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 3bdd23613..8944f309c 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,14 +1,14 @@ -import { lazy } from 'react'; import { Navigate, type RouteObject } from 'react-router-dom'; +import { safeLazy } from 'react-safe-lazy'; import { ApiResourceDetailsTabs } from '@/consts'; -const ApiResources = lazy(async () => import('@/pages/ApiResources')); -const ApiResourceDetails = lazy(async () => import('@/pages/ApiResourceDetails')); -const ApiResourcePermissions = lazy( +const ApiResources = safeLazy(async () => import('@/pages/ApiResources')); +const ApiResourceDetails = safeLazy(async () => import('@/pages/ApiResourceDetails')); +const ApiResourcePermissions = safeLazy( async () => import('@/pages/ApiResourceDetails/ApiResourcePermissions') ); -const ApiResourceSettings = lazy( +const ApiResourceSettings = safeLazy( async () => import('@/pages/ApiResourceDetails/ApiResourceSettings') ); 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 e4c178db1..bdb27c803 100644 --- a/packages/console/src/hooks/use-console-routes/routes/applications.tsx +++ b/packages/console/src/hooks/use-console-routes/routes/applications.tsx @@ -1,11 +1,11 @@ -import { lazy } from 'react'; import { Navigate, type RouteObject } from 'react-router-dom'; +import { safeLazy } from 'react-safe-lazy'; import { ApplicationDetailsTabs } from '@/consts'; -const Applications = lazy(async () => import('@/pages/Applications')); -const ApplicationDetails = lazy(async () => import('@/pages/ApplicationDetails')); -const AuditLogDetails = lazy(async () => import('@/pages/AuditLogDetails')); +const Applications = safeLazy(async () => import('@/pages/Applications')); +const ApplicationDetails = safeLazy(async () => import('@/pages/ApplicationDetails')); +const AuditLogDetails = safeLazy(async () => import('@/pages/AuditLogDetails')); export const applications: RouteObject = { path: 'applications', 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 e52934211..626c91527 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,8 +1,8 @@ -import { lazy } from 'react'; import { type RouteObject } from 'react-router-dom'; +import { safeLazy } from 'react-safe-lazy'; -const AuditLogs = lazy(async () => import('@/pages/AuditLogs')); -const AuditLogDetails = lazy(async () => import('@/pages/AuditLogDetails')); +const AuditLogs = safeLazy(async () => import('@/pages/AuditLogs')); +const AuditLogDetails = safeLazy(async () => import('@/pages/AuditLogDetails')); export const auditLogs: RouteObject = { path: 'audit-logs', 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 dbe359ba2..f79079393 100644 --- a/packages/console/src/hooks/use-console-routes/routes/connectors.tsx +++ b/packages/console/src/hooks/use-console-routes/routes/connectors.tsx @@ -1,10 +1,10 @@ -import { lazy } from 'react'; import { Navigate } from 'react-router-dom'; +import { safeLazy } from 'react-safe-lazy'; import { ConnectorsTabs } from '@/consts'; -const Connectors = lazy(async () => import('@/pages/Connectors')); -const ConnectorDetails = lazy(async () => import('@/pages/ConnectorDetails')); +const Connectors = safeLazy(async () => import('@/pages/Connectors')); +const ConnectorDetails = safeLazy(async () => import('@/pages/ConnectorDetails')); export const connectors = { path: 'connectors', 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 b5d8ec037..24b3fe44c 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,8 +1,8 @@ -import { lazy } from 'react'; import { type RouteObject } from 'react-router-dom'; +import { safeLazy } from 'react-safe-lazy'; -const CustomizeJwt = lazy(async () => import('@/pages/CustomizeJwt')); -const CustomizeJwtDetails = lazy(async () => import('@/pages/CustomizeJwtDetails')); +const CustomizeJwt = safeLazy(async () => import('@/pages/CustomizeJwt')); +const CustomizeJwtDetails = safeLazy(async () => import('@/pages/CustomizeJwtDetails')); export const customizeJwt: RouteObject = { path: 'customize-jwt', 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 caa7c1743..a70e61975 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,10 +1,10 @@ -import { lazy } from 'react'; import { Navigate, type RouteObject } from 'react-router-dom'; +import { safeLazy } from 'react-safe-lazy'; import { EnterpriseSsoDetailsTabs } from '@/consts/page-tabs'; -const EnterpriseSso = lazy(async () => import('@/pages/EnterpriseSso')); -const EnterpriseSsoDetails = lazy(async () => import('@/pages/EnterpriseSsoDetails')); +const EnterpriseSso = safeLazy(async () => import('@/pages/EnterpriseSso')); +const EnterpriseSsoDetails = safeLazy(async () => import('@/pages/EnterpriseSsoDetails')); export const enterpriseSso: RouteObject = { path: 'enterprise-sso', 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 0ef3fde5a..b35ccdb7f 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,6 @@ -import { lazy } from 'react'; import { type RouteObject } from 'react-router-dom'; +import { safeLazy } from 'react-safe-lazy'; -const Mfa = lazy(async () => import('@/pages/Mfa')); +const Mfa = safeLazy(async () => import('@/pages/Mfa')); export const mfa: RouteObject = { path: 'mfa', element: }; 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 aa69cf6e3..6242c363c 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,18 +1,18 @@ -import { lazy } from 'react'; import { Navigate, type RouteObject } from 'react-router-dom'; +import { safeLazy } from 'react-safe-lazy'; import { OrganizationRoleDetailsTabs, OrganizationTemplateTabs } from '@/consts'; -const OrganizationTemplate = lazy(async () => import('@/pages/OrganizationTemplate')); -const OrganizationRoles = lazy( +const OrganizationTemplate = safeLazy(async () => import('@/pages/OrganizationTemplate')); +const OrganizationRoles = safeLazy( async () => import('@/pages/OrganizationTemplate/OrganizationRoles') ); -const OrganizationPermissions = lazy( +const OrganizationPermissions = safeLazy( async () => import('@/pages/OrganizationTemplate/OrganizationPermissions') ); -const OrganizationRoleDetails = lazy(async () => import('@/pages/OrganizationRoleDetails')); -const Permissions = lazy(async () => import('@/pages/OrganizationRoleDetails/Permissions')); -const Settings = lazy(async () => import('@/pages/OrganizationRoleDetails/Settings')); +const OrganizationRoleDetails = safeLazy(async () => import('@/pages/OrganizationRoleDetails')); +const Permissions = safeLazy(async () => import('@/pages/OrganizationRoleDetails/Permissions')); +const Settings = safeLazy(async () => import('@/pages/OrganizationRoleDetails/Settings')); export const organizationTemplate: RouteObject[] = [ { 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 c8c95668a..20f73d6c9 100644 --- a/packages/console/src/hooks/use-console-routes/routes/organizations.tsx +++ b/packages/console/src/hooks/use-console-routes/routes/organizations.tsx @@ -1,14 +1,16 @@ import { condArray } from '@silverhand/essentials'; -import { lazy } from 'react'; import { Navigate, type RouteObject } from 'react-router-dom'; +import { safeLazy } from 'react-safe-lazy'; import { OrganizationDetailsTabs } from '@/pages/OrganizationDetails/types'; -const Organizations = lazy(async () => import('@/pages/Organizations')); -const OrganizationDetails = lazy(async () => import('@/pages/OrganizationDetails')); -const MachineToMachine = lazy(async () => import('@/pages/OrganizationDetails/MachineToMachine')); -const Members = lazy(async () => import('@/pages/OrganizationDetails/Members')); -const Settings = lazy(async () => import('@/pages/OrganizationDetails/Settings')); +const Organizations = safeLazy(async () => import('@/pages/Organizations')); +const OrganizationDetails = safeLazy(async () => import('@/pages/OrganizationDetails')); +const MachineToMachine = safeLazy( + async () => import('@/pages/OrganizationDetails/MachineToMachine') +); +const Members = safeLazy(async () => import('@/pages/OrganizationDetails/Members')); +const Settings = safeLazy(async () => import('@/pages/OrganizationDetails/Settings')); export const organizations: RouteObject = { path: 'organizations', 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 423030de4..17cdb1776 100644 --- a/packages/console/src/hooks/use-console-routes/routes/profile.tsx +++ b/packages/console/src/hooks/use-console-routes/routes/profile.tsx @@ -1,14 +1,14 @@ -import { lazy } from 'react'; import { type RouteObject } from 'react-router-dom'; +import { safeLazy } from 'react-safe-lazy'; -const ChangePasswordModal = lazy( +const ChangePasswordModal = safeLazy( async () => import('@/pages/Profile/containers/ChangePasswordModal') ); -const LinkEmailModal = lazy(async () => import('@/pages/Profile/containers/LinkEmailModal')); -const VerificationCodeModal = lazy( +const LinkEmailModal = safeLazy(async () => import('@/pages/Profile/containers/LinkEmailModal')); +const VerificationCodeModal = safeLazy( async () => import('@/pages/Profile/containers/VerificationCodeModal') ); -const VerifyPasswordModal = lazy( +const VerifyPasswordModal = safeLazy( async () => import('@/pages/Profile/containers/VerifyPasswordModal') ); 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 51e38899e..73a1269eb 100644 --- a/packages/console/src/hooks/use-console-routes/routes/roles.tsx +++ b/packages/console/src/hooks/use-console-routes/routes/roles.tsx @@ -1,14 +1,14 @@ -import { lazy } from 'react'; import { Navigate, type RouteObject } from 'react-router-dom'; +import { safeLazy } from 'react-safe-lazy'; import { RoleDetailsTabs } from '@/consts/page-tabs'; -const Roles = lazy(async () => import('@/pages/Roles')); -const RoleDetails = lazy(async () => import('@/pages/RoleDetails')); -const RolePermissions = lazy(async () => import('@/pages/RoleDetails/RolePermissions')); -const RoleSettings = lazy(async () => import('@/pages/RoleDetails/RoleSettings')); -const RoleUsers = lazy(async () => import('@/pages/RoleDetails/RoleUsers')); -const RoleApplications = lazy(async () => import('@/pages/RoleDetails/RoleApplications')); +const Roles = safeLazy(async () => import('@/pages/Roles')); +const RoleDetails = safeLazy(async () => import('@/pages/RoleDetails')); +const RolePermissions = safeLazy(async () => import('@/pages/RoleDetails/RolePermissions')); +const RoleSettings = safeLazy(async () => import('@/pages/RoleDetails/RoleSettings')); +const RoleUsers = safeLazy(async () => import('@/pages/RoleDetails/RoleUsers')); +const RoleApplications = safeLazy(async () => import('@/pages/RoleDetails/RoleApplications')); export const roles: RouteObject = { path: 'roles', 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 63b4a2e8e..f0c705ff9 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,9 +1,9 @@ -import { lazy } from 'react'; import { Navigate, type RouteObject } from 'react-router-dom'; +import { safeLazy } from 'react-safe-lazy'; import { SignInExperienceTab } from '@/pages/SignInExperience/types'; -const SignInExperience = lazy(async () => import('@/pages/SignInExperience')); +const SignInExperience = safeLazy(async () => import('@/pages/SignInExperience')); export const signInExperience: RouteObject = { path: 'sign-in-experience', 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 5c025626e..767d9c335 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,22 +1,27 @@ import { condArray } from '@silverhand/essentials'; -import { lazy, useContext, useMemo } from 'react'; +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'; -const TenantSettings = lazy(async () => import('@/pages/TenantSettings')); -const TenantBasicSettings = lazy(async () => import('@/pages/TenantSettings/TenantBasicSettings')); -const TenantDomainSettings = lazy( +const TenantSettings = safeLazy(async () => import('@/pages/TenantSettings')); +const TenantBasicSettings = safeLazy( + async () => import('@/pages/TenantSettings/TenantBasicSettings') +); +const TenantDomainSettings = safeLazy( async () => import('@/pages/TenantSettings/TenantDomainSettings') ); -const TenantMembers = lazy(async () => import('@/pages/TenantSettings/TenantMembers')); -const Invitations = lazy(async () => import('@/pages/TenantSettings/TenantMembers/Invitations')); -const Members = lazy(async () => import('@/pages/TenantSettings/TenantMembers/Members')); -const BillingHistory = lazy(async () => import('@/pages/TenantSettings/BillingHistory')); -const Subscription = lazy(async () => import('@/pages/TenantSettings/Subscription')); +const TenantMembers = safeLazy(async () => import('@/pages/TenantSettings/TenantMembers')); +const Invitations = safeLazy( + async () => import('@/pages/TenantSettings/TenantMembers/Invitations') +); +const Members = safeLazy(async () => import('@/pages/TenantSettings/TenantMembers/Members')); +const BillingHistory = safeLazy(async () => import('@/pages/TenantSettings/BillingHistory')); +const Subscription = safeLazy(async () => import('@/pages/TenantSettings/Subscription')); export const useTenantSettings = () => { const { isDevTenant } = useContext(TenantsContext); 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 2a4d30f99..0bad4fc52 100644 --- a/packages/console/src/hooks/use-console-routes/routes/users.tsx +++ b/packages/console/src/hooks/use-console-routes/routes/users.tsx @@ -1,15 +1,15 @@ -import { lazy } from 'react'; import { Navigate, type RouteObject } from 'react-router-dom'; +import { safeLazy } from 'react-safe-lazy'; import { UserDetailsTabs } from '@/consts/page-tabs'; -const AuditLogDetails = lazy(async () => import('@/pages/AuditLogDetails')); -const UserDetails = lazy(async () => import('@/pages/UserDetails')); -const UserLogs = lazy(async () => import('@/pages/UserDetails/UserLogs')); -const UserOrganizations = lazy(async () => import('@/pages/UserDetails/UserOrganizations')); -const UserRoles = lazy(async () => import('@/pages/UserDetails/UserRoles')); -const UserSettings = lazy(async () => import('@/pages/UserDetails/UserSettings')); -const Users = lazy(async () => import('@/pages/Users')); +const AuditLogDetails = safeLazy(async () => import('@/pages/AuditLogDetails')); +const UserDetails = safeLazy(async () => import('@/pages/UserDetails')); +const UserLogs = safeLazy(async () => import('@/pages/UserDetails/UserLogs')); +const UserOrganizations = safeLazy(async () => import('@/pages/UserDetails/UserOrganizations')); +const UserRoles = safeLazy(async () => import('@/pages/UserDetails/UserRoles')); +const UserSettings = safeLazy(async () => import('@/pages/UserDetails/UserSettings')); +const Users = safeLazy(async () => import('@/pages/Users')); export const users: RouteObject = { path: 'users', 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 e98231879..2cc47ee46 100644 --- a/packages/console/src/hooks/use-console-routes/routes/webhooks.tsx +++ b/packages/console/src/hooks/use-console-routes/routes/webhooks.tsx @@ -1,13 +1,13 @@ -import { lazy } from 'react'; import { Navigate, type RouteObject } from 'react-router-dom'; +import { safeLazy } from 'react-safe-lazy'; import { WebhookDetailsTabs } from '@/consts'; -const WebhookDetails = lazy(async () => import('@/pages/WebhookDetails')); -const AuditLogDetails = lazy(async () => import('@/pages/AuditLogDetails')); -const WebhookSettings = lazy(async () => import('@/pages/WebhookDetails/WebhookSettings')); -const WebhookLogs = lazy(async () => import('@/pages/WebhookDetails/WebhookLogs')); -const Webhooks = lazy(async () => import('@/pages/Webhooks')); +const WebhookDetails = safeLazy(async () => import('@/pages/WebhookDetails')); +const AuditLogDetails = safeLazy(async () => import('@/pages/AuditLogDetails')); +const WebhookSettings = safeLazy(async () => import('@/pages/WebhookDetails/WebhookSettings')); +const WebhookLogs = safeLazy(async () => import('@/pages/WebhookDetails/WebhookLogs')); +const Webhooks = safeLazy(async () => import('@/pages/Webhooks')); export const webhooks: RouteObject = { path: 'webhooks', diff --git a/packages/integration-tests/src/tests/console/error-handling.test.ts b/packages/integration-tests/src/tests/console/error-handling.test.ts new file mode 100644 index 000000000..a8614686c --- /dev/null +++ b/packages/integration-tests/src/tests/console/error-handling.test.ts @@ -0,0 +1,46 @@ +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(); + + 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'); + + trace.reset(expectConsole.page); + await trace.start(); + await expectConsole.navigateTo(path); + await trace.stop(); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any + const traceData: { traceEvents: any[] } = await trace.read(); + + const documentLoads = traceData.traceEvents.filter((item) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const data = item?.args?.data ?? {}; + return ( + data.resourceType === 'Document' && data.requestMethod === 'GET' && data.url === path.href + ); + }); + + // Reloaded once + expect(documentLoads).toHaveLength(2); + + // Show the error message + await Promise.all([ + expectConsole.toMatchElement('label', { + text: 'Oops! Something went wrong.', + }), + expectConsole.toMatchElement('span', { + text: 'Failed to fetch dynamically imported module', + }), + expectConsole.toMatchElement('button', { + text: 'Try again', + }), + ]); + }); +}); diff --git a/packages/integration-tests/src/tests/experience/server-side-rendering.test.ts b/packages/integration-tests/src/tests/experience/server-side-rendering.test.ts index 265c76713..e41ef12b3 100644 --- a/packages/integration-tests/src/tests/experience/server-side-rendering.test.ts +++ b/packages/integration-tests/src/tests/experience/server-side-rendering.test.ts @@ -1,14 +1,10 @@ -import fs from 'node:fs/promises'; -import os from 'node:os'; -import path from 'node:path'; - import { demoAppApplicationId, fullSignInExperienceGuard } from '@logto/schemas'; -import { type Page } from 'puppeteer'; import { z } from 'zod'; import { demoAppUrl } from '#src/constants.js'; import { OrganizationApiTest } from '#src/helpers/organization.js'; import ExpectExperience from '#src/ui-helpers/expect-experience.js'; +import { Trace } from '#src/ui-helpers/trace.js'; const ssrDataGuard = z.object({ signInExperience: z.object({ @@ -22,53 +18,6 @@ const ssrDataGuard = z.object({ }), }); -class Trace { - protected tracePath?: string; - - constructor(protected page?: Page) {} - - async start() { - if (this.tracePath) { - throw new Error('Trace already started'); - } - - if (!this.page) { - throw new Error('Page not set'); - } - - const traceDirectory = await fs.mkdtemp(path.join(os.tmpdir(), 'trace-')); - this.tracePath = path.join(traceDirectory, 'trace.json'); - await this.page.tracing.start({ path: this.tracePath, categories: ['devtools.timeline'] }); - } - - async stop() { - if (!this.page) { - throw new Error('Page not set'); - } - - return this.page.tracing.stop(); - } - - async read() { - if (!this.tracePath) { - throw new Error('Trace not started'); - } - - return JSON.parse(await fs.readFile(this.tracePath, 'utf8')); - } - - reset(page: Page) { - this.page = page; - this.tracePath = undefined; - } - - async cleanup() { - if (this.tracePath) { - await fs.unlink(this.tracePath); - } - } -} - describe('server-side rendering', () => { const trace = new Trace(); const expectTraceNotToHaveWellKnownEndpoints = async () => { diff --git a/packages/integration-tests/src/ui-helpers/trace.ts b/packages/integration-tests/src/ui-helpers/trace.ts new file mode 100644 index 000000000..fc37aeda5 --- /dev/null +++ b/packages/integration-tests/src/ui-helpers/trace.ts @@ -0,0 +1,54 @@ +import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; + +import { type Page } from 'puppeteer'; + +export class Trace { + protected tracePath?: string; + + constructor(protected page?: Page) {} + + async start() { + if (this.tracePath) { + throw new Error('Trace already started'); + } + + if (!this.page) { + throw new Error('Page not set'); + } + + const traceDirectory = await fs.mkdtemp(path.join(os.tmpdir(), 'trace-')); + this.tracePath = path.join(traceDirectory, 'trace.json'); + await this.page.tracing.start({ path: this.tracePath, categories: ['devtools.timeline'] }); + } + + async stop() { + if (!this.page) { + throw new Error('Page not set'); + } + + console.log('Trace captured at', this.tracePath); + return this.page.tracing.stop(); + } + + async read() { + if (!this.tracePath) { + throw new Error('Trace not started'); + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return JSON.parse(await fs.readFile(this.tracePath, 'utf8')); + } + + reset(page: Page) { + this.page = page; + this.tracePath = undefined; + } + + async cleanup() { + if (this.tracePath) { + await fs.unlink(this.tracePath); + } + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 86b76b1e1..d46ec3066 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