0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

refactor(console): safely lazy load pages (#6332)

* refactor(console): safely lazy load pages

* chore(console): use react-safe-lazy
This commit is contained in:
Gao Sun 2024-07-26 12:42:47 +08:00 committed by GitHub
parent ac40ef17d7
commit c45a1a5ad4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 257 additions and 164 deletions

View file

@ -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: {

View file

@ -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",

View file

@ -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<AppContentOutletContext>();

View file

@ -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 (
<Routes>
{/**
* 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 && <Route path="/" element={<Navigate to={ossConsolePath} />} />}
<Route path="/:tenantId" element={<Layout />}>
<Route path="callback" element={<Callback />} />
<Route path="welcome" element={<Welcome />} />
<Route element={<ProtectedRoutes />}>
<Route path={dropLeadingSlash(GlobalRoute.Profile) + '/*'} element={<Profile />} />
<Route element={<TenantAccess />}>
{isCloud && (
<Route
path={dropLeadingSlash(GlobalRoute.CheckoutSuccessCallback)}
element={<CheckoutSuccessCallback />}
/>
)}
<Route element={<AppContent />}>
<Route index element={<RedirectToFirstItem />} />
<Route path="*" element={<ConsoleContent />} />
<Suspense fallback={<AppLoading />}>
<Routes>
{/**
* 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 && <Route path="/" element={<Navigate to={ossConsolePath} />} />}
<Route path="/:tenantId" element={<Layout />}>
<Route path="callback" element={<Callback />} />
<Route path="welcome" element={<Welcome />} />
{isDevFeaturesEnabled && (
<Route path="__internal__/import-error" element={<__Internal__ImportError />} />
)}
<Route element={<ProtectedRoutes />}>
<Route path={dropLeadingSlash(GlobalRoute.Profile) + '/*'} element={<Profile />} />
<Route element={<TenantAccess />}>
{isCloud && (
<Route
path={dropLeadingSlash(GlobalRoute.CheckoutSuccessCallback)}
element={<CheckoutSuccessCallback />}
/>
)}
<Route element={<AppContent />}>
<Route index element={<RedirectToFirstItem />} />
<Route path="*" element={<ConsoleContent />} />
</Route>
</Route>
</Route>
</Route>
</Route>
</Routes>
</Routes>
</Suspense>
);
}

View file

@ -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;
});

View file

@ -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();

View file

@ -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')
);

View file

@ -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',

View file

@ -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',

View file

@ -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',

View file

@ -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',

View file

@ -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',

View file

@ -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: <Mfa /> };

View file

@ -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[] = [
{

View file

@ -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',

View file

@ -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')
);

View file

@ -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',

View file

@ -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',

View file

@ -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);

View file

@ -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',

View file

@ -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',

View file

@ -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',
}),
]);
});
});

View file

@ -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 () => {

View file

@ -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);
}
}
}

View file

@ -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