From b8d02ec35d44436789b32e97e01d671a44a53200 Mon Sep 17 00:00:00 2001 From: Peter Beverloo Date: Sat, 8 Feb 2025 22:39:47 +0000 Subject: [PATCH] infra: Enable eslint-plugin-unused-imports on the project --- .eslintrc.json | 13 ++++++++++++- app/admin/AdminTheme.tsx | 2 +- app/admin/components/ExpandableSection.tsx | 2 +- app/admin/content/ContentEditorMdx.tsx | 4 ++-- app/admin/events/[event]/EventSalesGraph.tsx | 2 +- app/admin/events/[event]/EventTeamCard.tsx | 3 +-- .../[event]/[team]/applications/Header.tsx | 2 +- .../[event]/[team]/schedule/ScheduleImpl.tsx | 6 ++---- .../events/[event]/[team]/shifts/ShiftTable.tsx | 2 +- app/admin/events/[event]/[team]/shifts/page.tsx | 2 +- .../volunteers/[volunteer]/VolunteerHeader.tsx | 2 +- .../events/[event]/program/requests/page.tsx | 2 +- .../settings/EventParticipatingTeams.tsx | 2 -- app/admin/events/page.tsx | 2 +- app/admin/volunteers/layout.tsx | 3 +-- app/api/Action.test.ts | 1 - .../admin/program/activities/[[...id]]/route.ts | 2 +- app/api/ai/updateSettings.ts | 2 +- app/api/event/schedule/PublicSchedule.ts | 2 +- app/api/webhook/twilio/inbound/route.ts | 1 + app/components/Markdown.tsx | 1 - app/display/DisplayController.tsx | 1 + app/display/cards/ActiveVolunteersCard.tsx | 1 - app/lib/auth/AuthenticationContext.test.ts | 3 --- .../training/TrainingPreferencesForm.tsx | 2 -- .../[slug]/application/training/page.tsx | 2 +- .../authentication/RegisterDialog.tsx | 1 + app/schedule/[event]/ScheduleContextManager.tsx | 1 + app/schedule/[event]/ScheduleTheme.tsx | 2 +- app/schedule/[event]/components/Header.tsx | 4 +--- package-lock.json | 17 +++++++++++++++++ package.json | 1 + 32 files changed, 55 insertions(+), 38 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 69223f3e..667260b6 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,5 +1,8 @@ { "extends": "next", + "plugins": [ + "unused-imports" + ], "rules": { "eol-last": [ "error", "always" ], "eqeqeq": [ "error", "always" ], @@ -24,6 +27,14 @@ "prefer-const": "warn", "quotes": [ "error", "single" ], "react/jsx-indent-props": "off", - "react/no-unescaped-entities": "off" + "react/no-unescaped-entities": "off", + "unused-imports/no-unused-imports": "error", + "unused-imports/no-unused-vars": [ + "warn", + { + "args": "none", + "caughtErrors": "none" + } + ] } } diff --git a/app/admin/AdminTheme.tsx b/app/admin/AdminTheme.tsx index 56dec93c..dc952d61 100644 --- a/app/admin/AdminTheme.tsx +++ b/app/admin/AdminTheme.tsx @@ -5,7 +5,7 @@ import type { PaletteMode } from '@mui/material'; import type { Theme, ThemeOptions } from '@mui/material/styles'; -import { createTheme, darken, lighten } from '@mui/material/styles'; +import { createTheme } from '@mui/material/styles'; import { deepmerge } from '@mui/utils'; import grey from '@mui/material/colors/grey'; diff --git a/app/admin/components/ExpandableSection.tsx b/app/admin/components/ExpandableSection.tsx index 8baabd0d..ee012028 100644 --- a/app/admin/components/ExpandableSection.tsx +++ b/app/admin/components/ExpandableSection.tsx @@ -56,7 +56,7 @@ export function ExpandableSection(props: React.PropsWithChildren - {props.children} + {children} diff --git a/app/admin/content/ContentEditorMdx.tsx b/app/admin/content/ContentEditorMdx.tsx index e272fbc6..4094f897 100644 --- a/app/admin/content/ContentEditorMdx.tsx +++ b/app/admin/content/ContentEditorMdx.tsx @@ -13,8 +13,8 @@ import type { JsxComponentDescriptor, MDXEditorMethods } from '@mdxeditor/editor import { BlockTypeSelect, BoldItalicUnderlineToggles, CreateLink, DiffSourceToggleWrapper, GenericJsxEditor, ListsToggle, MDXEditor, Separator, UndoRedo, diffSourcePlugin, headingsPlugin, - imagePlugin, jsxPlugin, linkPlugin, listsPlugin, markdownShortcutPlugin, quotePlugin, - tablePlugin, thematicBreakPlugin, toolbarPlugin } from '@mdxeditor/editor'; + jsxPlugin, linkPlugin, listsPlugin, markdownShortcutPlugin, quotePlugin, tablePlugin, + thematicBreakPlugin, toolbarPlugin } from '@mdxeditor/editor'; import { unrecognisedNodePlugin } from './mdxEditorPlugins'; diff --git a/app/admin/events/[event]/EventSalesGraph.tsx b/app/admin/events/[event]/EventSalesGraph.tsx index 1b8e2f21..55ed5277 100644 --- a/app/admin/events/[event]/EventSalesGraph.tsx +++ b/app/admin/events/[event]/EventSalesGraph.tsx @@ -162,7 +162,7 @@ export function EventSalesGraph(props: EventSalesGraphProps) { .call(formattedLeftAxis(scaleY)); // Create a clip path: - const clip = element.append('defs') + element.append('defs') .append('SVG:clipPath') .attr('id', 'clip') .append('SVG:rect') diff --git a/app/admin/events/[event]/EventTeamCard.tsx b/app/admin/events/[event]/EventTeamCard.tsx index e840bacb..7dba39d4 100644 --- a/app/admin/events/[event]/EventTeamCard.tsx +++ b/app/admin/events/[event]/EventTeamCard.tsx @@ -35,8 +35,7 @@ interface TeamIdentityHeaderProps extends BoxProps { * with a font colour that provides an appropriate amount of context. */ const TeamIdentityHeader = styled((props: TeamIdentityHeaderProps) => { - const { darkThemeColour, lightThemeColour, ...rest } = props; - return ; + return ; })(({ darkThemeColour, lightThemeColour, theme }) => { const backgroundColor = theme.palette.mode === 'light' ? lightThemeColour : darkThemeColour; const color = theme.palette.getContrastText(backgroundColor); diff --git a/app/admin/events/[event]/[team]/applications/Header.tsx b/app/admin/events/[event]/[team]/applications/Header.tsx index baf5e100..2eb39ffe 100644 --- a/app/admin/events/[event]/[team]/applications/Header.tsx +++ b/app/admin/events/[event]/[team]/applications/Header.tsx @@ -34,7 +34,7 @@ interface HeaderProps { * applications are no longer being accepted. These settings can be changed in Event Settings. */ export function Header(props: HeaderProps) { - const { event, team, user } = props; + const { event, team } = props; return ( diff --git a/app/admin/events/[event]/[team]/schedule/ScheduleImpl.tsx b/app/admin/events/[event]/[team]/schedule/ScheduleImpl.tsx index eb601b94..93d51b10 100644 --- a/app/admin/events/[event]/[team]/schedule/ScheduleImpl.tsx +++ b/app/admin/events/[event]/[team]/schedule/ScheduleImpl.tsx @@ -291,10 +291,8 @@ export function ScheduleImpl(props: ScheduleImplProps) { let estimatedScheduleHeight = /* header= */ 50; for (const resource of resources) { estimatedScheduleHeight += /* section header= */ 32; - if (!!resource.children) { - for (const child of resource.children) - estimatedScheduleHeight += /* resource =*/ 32; - } + if (!!resource.children) + estimatedScheduleHeight += resource.children.length * /* resource =*/ 32; } // Substract 100 from the `windowHeight` to ensure that the essential UI around the tool diff --git a/app/admin/events/[event]/[team]/shifts/ShiftTable.tsx b/app/admin/events/[event]/[team]/shifts/ShiftTable.tsx index 6300f75c..83cbaa3f 100644 --- a/app/admin/events/[event]/[team]/shifts/ShiftTable.tsx +++ b/app/admin/events/[event]/[team]/shifts/ShiftTable.tsx @@ -11,7 +11,7 @@ import NewReleasesIcon from '@mui/icons-material/NewReleases'; import PaletteIcon from '@mui/icons-material/Palette'; import SentimentSatisfiedAltIcon from '@mui/icons-material/SentimentSatisfiedAlt'; import Tooltip from '@mui/material/Tooltip'; -import Typography, { type TypographyProps } from '@mui/material/Typography'; +import Typography from '@mui/material/Typography'; import type { EventShiftContext, EventShiftRowModel } from '@app/api/admin/event/shifts/[[...id]]/route'; import { ExcitementIcon } from '@app/admin/components/ExcitementIcon'; diff --git a/app/admin/events/[event]/[team]/shifts/page.tsx b/app/admin/events/[event]/[team]/shifts/page.tsx index 6efc4fe1..2e00ed16 100644 --- a/app/admin/events/[event]/[team]/shifts/page.tsx +++ b/app/admin/events/[event]/[team]/shifts/page.tsx @@ -22,7 +22,7 @@ import { verifyAccessAndFetchPageInfo } from '@app/admin/events/verifyAccessAndF * end up out of sync. */ export default async function EventTeamShiftsPage(props: NextPageParams<'event' | 'team'>) { - const { access, event, team, user } = await verifyAccessAndFetchPageInfo(props.params); + const { access, event, team } = await verifyAccessAndFetchPageInfo(props.params); const accessScope = { event: event.slug, team: team.slug }; diff --git a/app/admin/events/[event]/[team]/volunteers/[volunteer]/VolunteerHeader.tsx b/app/admin/events/[event]/[team]/volunteers/[volunteer]/VolunteerHeader.tsx index 57a4c49c..2feabfc0 100644 --- a/app/admin/events/[event]/[team]/volunteers/[volunteer]/VolunteerHeader.tsx +++ b/app/admin/events/[event]/[team]/volunteers/[volunteer]/VolunteerHeader.tsx @@ -402,7 +402,7 @@ interface VolunteerHeaderProps { * to change their participation. The exact actions depend on the access level of the user. */ export function VolunteerHeader(props: VolunteerHeaderProps) { - const { event, team, volunteer, user } = props; + const { event, team, volunteer } = props; const allowSilent = props.canUpdateWithoutNotification; diff --git a/app/admin/events/[event]/program/requests/page.tsx b/app/admin/events/[event]/program/requests/page.tsx index ad1e1a73..e29dbd2a 100644 --- a/app/admin/events/[event]/program/requests/page.tsx +++ b/app/admin/events/[event]/program/requests/page.tsx @@ -14,7 +14,7 @@ import { kRegistrationStatus } from '@lib/database/Types'; * help from the volunteering teams. Requests must be managed directly by our team. */ export default async function ProgramRequestsPage(props: NextPageParams<'event'>) { - const { access, event, user } = await verifyAccessAndFetchPageInfo(props.params); + const { access, event } = await verifyAccessAndFetchPageInfo(props.params); const leaders = await db.selectFrom(tUsersEvents) .innerJoin(tRoles) diff --git a/app/admin/events/[event]/settings/EventParticipatingTeams.tsx b/app/admin/events/[event]/settings/EventParticipatingTeams.tsx index 9a3a7a67..98f53363 100644 --- a/app/admin/events/[event]/settings/EventParticipatingTeams.tsx +++ b/app/admin/events/[event]/settings/EventParticipatingTeams.tsx @@ -26,8 +26,6 @@ interface EventParticipatingTeamsProps { * will enable a more detailed section with settings specific to that team. */ export function EventParticipatingTeams(props: EventParticipatingTeamsProps) { - const { event } = props; - const context = { event: props.event.slug }; const columns: RemoteDataTableColumn[] = [ { diff --git a/app/admin/events/page.tsx b/app/admin/events/page.tsx index 47458051..b210efde 100644 --- a/app/admin/events/page.tsx +++ b/app/admin/events/page.tsx @@ -17,7 +17,7 @@ import { kAnyEvent, kAnyTeam } from '@lib/auth/AccessControl'; * events. Events cannot be removed through the portal, although they can be hidden. */ export default async function EventsPage() { - const { access, user } = await requireAuthenticationContext({ + const { access } = await requireAuthenticationContext({ check: 'admin', permission: { permission: 'event.visible', diff --git a/app/admin/volunteers/layout.tsx b/app/admin/volunteers/layout.tsx index fa28927b..ec6a06e4 100644 --- a/app/admin/volunteers/layout.tsx +++ b/app/admin/volunteers/layout.tsx @@ -17,9 +17,8 @@ import { or, requireAuthenticationContext } from '@lib/auth/AuthenticationContex * (signed in) user, although the available options will depend on the user's access level. */ export default async function VolunteersLayout(props: React.PropsWithChildren) { - // Note: keep this in sync with //admin/layout.tsx - const { access, user } = await requireAuthenticationContext({ + const { access } = await requireAuthenticationContext({ check: 'admin', permission: or( 'volunteer.export', diff --git a/app/api/Action.test.ts b/app/api/Action.test.ts index b45561ea..51b2e2aa 100644 --- a/app/api/Action.test.ts +++ b/app/api/Action.test.ts @@ -2,7 +2,6 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. import { NextRequest } from 'next/server'; -import { forbidden } from 'next/navigation'; import { serialize } from 'cookie'; import { z } from 'zod'; diff --git a/app/api/admin/program/activities/[[...id]]/route.ts b/app/api/admin/program/activities/[[...id]]/route.ts index 1f0b9757..b3add0dd 100644 --- a/app/api/admin/program/activities/[[...id]]/route.ts +++ b/app/api/admin/program/activities/[[...id]]/route.ts @@ -290,7 +290,7 @@ createDataTableApi(kProgramActivityRowModel, kProgramActivityContext, { locationId = activity.locationId; } else if (!!activity.timeslots.length) { const uniqueLocations = new Set(); - for (const { locationId, locationName } of activity.timeslots) + for (const { locationId } of activity.timeslots) uniqueLocations.add(locationId); if (uniqueLocations.size === 1) { diff --git a/app/api/ai/updateSettings.ts b/app/api/ai/updateSettings.ts index b842795f..e8595f35 100644 --- a/app/api/ai/updateSettings.ts +++ b/app/api/ai/updateSettings.ts @@ -7,7 +7,7 @@ import type { ActionProps } from '../Action'; import type { ApiDefinition, ApiRequest, ApiResponse } from '../Types'; import { Log, kLogSeverity, kLogType } from '@lib/Log'; import { executeAccessCheck } from '@lib/auth/AuthenticationContext'; -import { writeSetting, writeSettings } from '@lib/Settings'; +import { writeSettings } from '@lib/Settings'; /** * Interface definition for the Generative AI API, exposed through /api/ai. diff --git a/app/api/event/schedule/PublicSchedule.ts b/app/api/event/schedule/PublicSchedule.ts index 085d5a76..906f849a 100644 --- a/app/api/event/schedule/PublicSchedule.ts +++ b/app/api/event/schedule/PublicSchedule.ts @@ -3,7 +3,7 @@ import { z } from 'zod'; -import { type VendorTeam, kRoleBadge, kVendorTeam as kVendorTeamEnum } from '@lib/database/Types'; +import { kRoleBadge, kVendorTeam as kVendorTeamEnum } from '@lib/database/Types'; /** * Represents the information shared for a particular vendor team. The actual information regarding diff --git a/app/api/webhook/twilio/inbound/route.ts b/app/api/webhook/twilio/inbound/route.ts index 1223f352..3c34a172 100644 --- a/app/api/webhook/twilio/inbound/route.ts +++ b/app/api/webhook/twilio/inbound/route.ts @@ -14,6 +14,7 @@ import { kTwilioWebhookEndpoint } from '@lib/database/Types'; * generally either SMS or WhatsApp. An immediate response is expected. */ export async function POST(request: NextRequest) { + // eslint-disable-next-line unused-imports/no-unused-vars const { authenticated, body } = await authenticateAndRecordTwilioRequest(request, kTwilioWebhookEndpoint.Inbound); diff --git a/app/components/Markdown.tsx b/app/components/Markdown.tsx index d58e019b..2e142c39 100644 --- a/app/components/Markdown.tsx +++ b/app/components/Markdown.tsx @@ -12,7 +12,6 @@ import Alert from '@mui/material/Alert'; import Box, { type BoxProps } from '@mui/material/Box'; import { default as MuiLink, type LinkProps } from '@mui/material/Link'; import Typography, { type TypographyProps } from '@mui/material/Typography'; -import { darken, lighten } from '@mui/material/styles'; import { RemoteContent } from './RemoteContent'; diff --git a/app/display/DisplayController.tsx b/app/display/DisplayController.tsx index 2cd44358..7b9daa75 100644 --- a/app/display/DisplayController.tsx +++ b/app/display/DisplayController.tsx @@ -52,6 +52,7 @@ export function DisplayController(props: React.PropsWithChildren) { // Periodically update the display's configuration using the `SWR` library. The interval can be // configured by the server, although will default to one update per five minutes. + // eslint-disable-next-line unused-imports/no-unused-vars const { data, error, isLoading, mutate } = useSWR(url, fetcher, { refreshInterval: data => !!data ? data.config.updateFrequencyMs : kDefaultUpdateFrequencyMs, }); diff --git a/app/display/cards/ActiveVolunteersCard.tsx b/app/display/cards/ActiveVolunteersCard.tsx index 3dade00f..1e978654 100644 --- a/app/display/cards/ActiveVolunteersCard.tsx +++ b/app/display/cards/ActiveVolunteersCard.tsx @@ -3,7 +3,6 @@ 'use client'; -import Box from '@mui/material/Box'; import Grid from '@mui/material/Grid2'; import Paper from '@mui/material/Paper'; import Stack from '@mui/material/Stack'; diff --git a/app/lib/auth/AuthenticationContext.test.ts b/app/lib/auth/AuthenticationContext.test.ts index cb6c379b..b550d031 100644 --- a/app/lib/auth/AuthenticationContext.test.ts +++ b/app/lib/auth/AuthenticationContext.test.ts @@ -1,9 +1,6 @@ // Copyright 2023 Peter Beverloo & AnimeCon. All rights reserved. // Use of this source code is governed by a MIT license that can be found in the LICENSE file. -import { getAccessFallbackHTTPStatus, isHTTPAccessFallbackError } - from 'next/dist/client/components/http-access-fallback/http-access-fallback'; - import { AccessControl } from './AccessControl'; import { type SessionData, kSessionCookieName, sealSession } from './Session'; import { executeAccessCheck, getAuthenticationContextFromHeaders } from './AuthenticationContext'; diff --git a/app/registration/[slug]/application/training/TrainingPreferencesForm.tsx b/app/registration/[slug]/application/training/TrainingPreferencesForm.tsx index 2f55688e..292a8434 100644 --- a/app/registration/[slug]/application/training/TrainingPreferencesForm.tsx +++ b/app/registration/[slug]/application/training/TrainingPreferencesForm.tsx @@ -7,8 +7,6 @@ import { useMemo } from 'react'; import { SelectElement } from '@proxy/react-hook-form-mui'; -import Grid from '@mui/material/Grid2'; - /** * Props accepted by the component. */ diff --git a/app/registration/[slug]/application/training/page.tsx b/app/registration/[slug]/application/training/page.tsx index abb0456d..63d60e7c 100644 --- a/app/registration/[slug]/application/training/page.tsx +++ b/app/registration/[slug]/application/training/page.tsx @@ -28,7 +28,7 @@ export default async function EventApplicationTrainingPage(props: NextPageParams if (!context || !context.registration || !context.user || !context.event.trainingEnabled) notFound(); // the event does not exist, or the volunteer is not signed in - const { access, environment, event, registration, slug, teamSlug, user } = context; + const { access, event, registration, slug, teamSlug, user } = context; const eligible = registration.trainingEligible; const override = access.can('event.trainings', { event: event.slug }); diff --git a/app/registration/authentication/RegisterDialog.tsx b/app/registration/authentication/RegisterDialog.tsx index ee3885cd..37420faf 100644 --- a/app/registration/authentication/RegisterDialog.tsx +++ b/app/registration/authentication/RegisterDialog.tsx @@ -68,6 +68,7 @@ export function RegisterDialog(props: RegisterDialogProps) { // client side to prevent sending it to the server altogether, the |rawBirthdate| because // we want to make sure that it's shared in a particular format, and the |username| because // it's only included in the form to help autofill providers in browsers. + // eslint-disable-next-line unused-imports/no-unused-vars const { rawBirthdate, username, password, ...rest } = data; // Format the |birthdate| in YYYY-MM-DD format because that's the only sensible format to diff --git a/app/schedule/[event]/ScheduleContextManager.tsx b/app/schedule/[event]/ScheduleContextManager.tsx index 7bdae951..9c80e915 100644 --- a/app/schedule/[event]/ScheduleContextManager.tsx +++ b/app/schedule/[event]/ScheduleContextManager.tsx @@ -47,6 +47,7 @@ export function ScheduleContextManager(props: React.PropsWithChildren(endpoint, scheduleFetcher, { refreshInterval: data => !!data ? data.config.updateFrequencyMs : kDefaultUpdateFrequencyMs, }); diff --git a/app/schedule/[event]/ScheduleTheme.tsx b/app/schedule/[event]/ScheduleTheme.tsx index c15481a9..d722864a 100644 --- a/app/schedule/[event]/ScheduleTheme.tsx +++ b/app/schedule/[event]/ScheduleTheme.tsx @@ -10,7 +10,7 @@ import type { SxProps } from '@mui/system'; import type { Theme } from '@mui/material/styles'; import { ThemeProvider } from '@mui/material/styles'; import { createTheme } from '@mui/material/styles'; -import { darken, decomposeColor, emphasize, lighten } from '@mui/system/colorManipulator'; +import { decomposeColor, lighten } from '@mui/system/colorManipulator'; import useMediaQuery from '@mui/material/useMediaQuery'; import Box from '@mui/material/Box'; diff --git a/app/schedule/[event]/components/Header.tsx b/app/schedule/[event]/components/Header.tsx index 2e45aaf8..7be56634 100644 --- a/app/schedule/[event]/components/Header.tsx +++ b/app/schedule/[event]/components/Header.tsx @@ -3,8 +3,6 @@ 'use client'; -import type { SxProps } from '@mui/system'; -import type { Theme } from '@mui/material/styles'; import ListItemText from '@mui/material/ListItemText'; import ListItem from '@mui/material/ListItem'; import List from '@mui/material/List'; @@ -37,7 +35,7 @@ interface HeaderProps { * of the scheduling app. The header is not actionable by default. */ export function Header(props: HeaderProps) { - const { title, subtitle, icon } = props; + const { title, subtitle } = props; return ( diff --git a/package-lock.json b/package-lock.json index 34372380..2e0b0f75 100644 --- a/package-lock.json +++ b/package-lock.json @@ -70,6 +70,7 @@ "cross-env": "^7.0.3", "eslint": "9.19.0", "eslint-config-next": "^15.1.6", + "eslint-plugin-unused-imports": "^4.1.4", "fs-extra": "^11.3.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", @@ -8432,6 +8433,22 @@ "semver": "bin/semver.js" } }, + "node_modules/eslint-plugin-unused-imports": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.1.4.tgz", + "integrity": "sha512-YptD6IzQjDardkl0POxnnRBhU1OEePMV0nd6siHaRBbd+lyh6NAhFEobiznKU7kTsSsDeSD62Pe7kAM1b7dAZQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0", + "eslint": "^9.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + } + } + }, "node_modules/eslint-scope": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", diff --git a/package.json b/package.json index c95c039e..c0c47a21 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ "cross-env": "^7.0.3", "eslint": "9.19.0", "eslint-config-next": "^15.1.6", + "eslint-plugin-unused-imports": "^4.1.4", "fs-extra": "^11.3.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0",