feat: AI description - DB model + frontend + backend (fetch only) (#17651)

* feat: AI description - DB model + frontend + backend (fetch only)

* fix types and add validation to backend

* improve log

* improve

* import type

* fix replexica error

* fix

* fix test

* update replexica type

* Renamed descriptionTranslations to fieldTranslations

* Moved the eventTypeId column to 2nd

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
This commit is contained in:
Benny Joo
2024-11-18 00:16:56 +00:00
committed by GitHub
co-authored by Keith Williams
parent f9afca2939
commit 5401bcca4e
20 changed files with 621 additions and 9 deletions
+3
View File
@@ -408,3 +408,6 @@ NEXT_PUBLIC_WEBSITE_TERMS_URL=
# NEXT_PUBLIC_LOGGER_LEVEL=3 sets to log info, warn, error and fatal logs.
# [0: silly & upwards, 1: trace & upwards, 2: debug & upwards, 3: info & upwards, 4: warn & upwards, 5: error & fatal, 6: fatal]
NEXT_PUBLIC_LOGGER_LEVEL=
# Used to use Replexica SDK, a tool for real-time AI-powered localization
REPLEXICA_API_KEY=
+1
View File
@@ -64,6 +64,7 @@
"@radix-ui/react-switch": "^1.0.0",
"@radix-ui/react-toggle-group": "^1.0.0",
"@radix-ui/react-tooltip": "^1.0.0",
"@replexica/sdk": "^0.6.0",
"@sentry/nextjs": "^8.8.0",
"@stripe/react-stripe-js": "^1.10.0",
"@stripe/stripe-js": "^1.35.0",
@@ -2791,6 +2791,7 @@
"salesforce_route_to_custom_lookup_field": "Route to a user that matches a lookup field on an account",
"salesforce_option": "Salesforce Option",
"lookup_field_name": "Lookup Field Name",
"translate_description_button": "Translate description to the visitor's browser language using AI",
"rr_distribution_method": "Distribution",
"rr_distribution_method_description": "Allows for optimising distribution for maximum availability or to aim for a more balanced assignment.",
"rr_distribution_method_availability_title": "Maximize availability",
@@ -112,6 +112,7 @@ describe("handleChildrenEventTypes", () => {
lockTimeZoneToggleOnBookingPage,
useEventTypeDestinationCalendarEmail,
secondaryEmailId,
autoTranslateDescriptionEnabled,
...evType
} = mockFindFirstEventType({
id: 123,
@@ -182,7 +183,7 @@ describe("handleChildrenEventTypes", () => {
bookingLimits: undefined,
},
});
const { profileId, ...rest } = evType;
const { profileId, autoTranslateDescriptionEnabled, ...rest } = evType;
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
data: {
...rest,
@@ -265,6 +266,7 @@ describe("handleChildrenEventTypes", () => {
lockTimeZoneToggleOnBookingPage,
useEventTypeDestinationCalendarEmail,
secondaryEmailId,
autoTranslateDescriptionEnabled,
...evType
} = mockFindFirstEventType({
id: 123,
@@ -337,7 +339,7 @@ describe("handleChildrenEventTypes", () => {
length: 30,
},
});
const { profileId, ...rest } = evType;
const { profileId, autoTranslateDescriptionEnabled, ...rest } = evType;
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
data: {
...rest,
@@ -378,6 +380,7 @@ describe("handleChildrenEventTypes", () => {
lockTimeZoneToggleOnBookingPage,
useEventTypeDestinationCalendarEmail,
secondaryEmailId,
autoTranslateDescriptionEnabled,
...evType
} = mockFindFirstEventType({
metadata: { managedEventConfig: {} },
@@ -48,6 +48,8 @@ export const EventMeta = ({
| "recurringEvent"
| "price"
| "isDynamic"
| "fieldTranslations"
| "autoTranslateDescriptionEnabled"
> | null;
isPending: boolean;
isPlatform?: boolean;
@@ -101,6 +103,10 @@ export const EventMeta = ({
: isHalfFull
? "text-yellow-500"
: "text-bookinghighlight";
const browserLocale = navigator.language; // e.g. "en-US", "es-ES", "fr-FR"
const translatedDescription = (event?.fieldTranslations ?? []).find((translation) =>
browserLocale.startsWith(translation.targetLang)
)?.translatedText;
return (
<div className={`${classNames?.eventMetaContainer || ""} relative z-10 p-6`} data-testid="event-meta">
@@ -120,9 +126,9 @@ export const EventMeta = ({
/>
)}
<EventTitle className={`${classNames?.eventMetaTitle} my-2`}>{event?.title}</EventTitle>
{event.description && (
{(event.description || translatedDescription) && (
<EventMetaBlock contentClassName="mb-8 break-words max-w-full max-h-[180px] scroll-bar pr-4">
<div dangerouslySetInnerHTML={{ __html: event.description }} />
<div dangerouslySetInnerHTML={{ __html: translatedDescription ?? event.description }} />
</EventMetaBlock>
)}
<div className="space-y-4 font-medium rtl:-mr-2">
+2
View File
@@ -51,6 +51,8 @@ export type BookerEvent = Pick<
| "bookingFields"
| "seatsShowAvailabilityCount"
| "isInstantEvent"
| "fieldTranslations"
| "autoTranslateDescriptionEnabled"
> & { users: BookerEventUser[]; showInstantEventConnectNowModal: boolean } & { profile: BookerEventProfile };
export type ValidationErrors<T extends object> = { key: FieldPath<T>; error: ErrorOption }[];
@@ -1,3 +1,4 @@
import { useSession } from "next-auth/react";
import { useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import type { UseFormGetValues, UseFormSetValue, Control, FormState } from "react-hook-form";
@@ -20,6 +21,7 @@ export type EventSetupTabProps = Pick<
>;
export const EventSetupTab = (props: EventSetupTabProps & { urlPrefix: string; hasOrgBranding: boolean }) => {
const { t } = useLocale();
const session = useSession();
const isPlatform = useIsPlatform();
const formMethods = useFormContext<FormValues>();
const { eventType, team, urlPrefix, hasOrgBranding } = props;
@@ -29,6 +31,7 @@ export const EventSetupTab = (props: EventSetupTabProps & { urlPrefix: string; h
const [firstRender, setFirstRender] = useState(true);
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
const autoTranslateDescriptionEnabled = formMethods.watch("autoTranslateDescriptionEnabled");
const multipleDurationOptions = [
5, 10, 15, 20, 25, 30, 45, 50, 60, 75, 80, 90, 120, 150, 180, 240, 300, 360, 420, 480,
@@ -95,6 +98,17 @@ export const EventSetupTab = (props: EventSetupTabProps & { urlPrefix: string; h
</>
)}
</div>
<div className="[&_label]:my-1 [&_label]:font-normal">
<SettingsToggle
title={t("translate_description_button")}
checked={!!autoTranslateDescriptionEnabled}
onCheckedChange={(value) => {
formMethods.setValue("autoTranslateDescriptionEnabled", value, { shouldDirty: true });
}}
disabled={!session.data?.user.org?.id}
tooltip={!session.data?.user.org?.id ? t("orgs_upgrade_to_enable_feature") : undefined}
/>
</div>
<TextField
required
label={isPlatform ? "Slug" : t("URL")}
@@ -66,6 +66,13 @@ const publicEventSelect = Prisma.validator<Prisma.EventTypeSelect>()({
metadata: true,
lockTimeZoneToggleOnBookingPage: true,
requiresConfirmation: true,
autoTranslateDescriptionEnabled: true,
fieldTranslations: {
select: {
translatedText: true,
targetLang: true,
},
},
requiresBookerEmailVerification: true,
recurringEvent: true,
price: true,
@@ -293,6 +300,8 @@ export const getPublicEvent = async (
},
isInstantEvent: false,
showInstantEventConnectNowModal: false,
autoTranslateDescriptionEnabled: false,
fieldTranslations: [],
};
}
@@ -2,6 +2,7 @@ import type { z } from "zod";
import type { EventLocationType } from "@calcom/core/location";
import type { ChildrenEventType } from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect";
import type { EventTypeTranslation } from "@calcom/prisma/client";
import type { PeriodType, SchedulingType } from "@calcom/prisma/enums";
import type { BookerLayoutSettings, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import type { customInputSchema } from "@calcom/prisma/zod-utils";
@@ -109,6 +110,8 @@ export type FormValues = {
seatsShowAttendees: boolean | null;
seatsShowAvailabilityCount: boolean | null;
seatsPerTimeSlotEnabled: boolean;
autoTranslateDescriptionEnabled: boolean;
fieldTranslations: EventTypeTranslation[];
scheduleName: string;
minimumBookingNotice: number;
minimumBookingNoticeInDurationType: number;
+2
View File
@@ -168,6 +168,7 @@ export const CALCOM_PRIVATE_API_ROUTE = process.env.CALCOM_PRIVATE_API_ROUTE ||
export const WEBSITE_PRIVACY_POLICY_URL =
process.env.NEXT_PUBLIC_WEBSITE_PRIVACY_POLICY_URL || "https://cal.com/privacy";
export const WEBSITE_TERMS_URL = process.env.NEXT_PUBLIC_WEBSITE_TERMS_URL || "https://cal.com/terms";
export const REPLEXICA_API_KEY = process.env.REPLEXICA_API_KEY;
/**
* The maximum number of days we should check for if we don't find all required bookable days
@@ -190,3 +191,4 @@ export const RECORDING_DEFAULT_ICON = IS_PRODUCTION
export const RECORDING_IN_PROGRESS_ICON = IS_PRODUCTION
? `${WEBAPP_URL}/stop-recording.svg`
: `https://app.cal.com/stop-recording.svg`;
+2
View File
@@ -110,6 +110,8 @@ const commons = {
useEventTypeDestinationCalendarEmail: false,
secondaryEmailId: null,
secondaryEmail: null,
autoTranslateDescriptionEnabled: false,
fieldTranslations: [],
maxLeadThreshold: null,
};
+1
View File
@@ -13,6 +13,7 @@ export const eventTypeSelect = Prisma.validator<Prisma.EventTypeSelect>()({
title: true,
requiresConfirmation: true,
requiresConfirmationWillBlockSlot: true,
autoTranslateDescriptionEnabled: true,
position: true,
offsetStart: true,
profileId: true,
@@ -445,6 +445,13 @@ export class EventTypeRepository {
requiresConfirmation: true,
requiresConfirmationWillBlockSlot: true,
requiresBookerEmailVerification: true,
autoTranslateDescriptionEnabled: true,
fieldTranslations: {
select: {
translatedText: true,
targetLang: true,
},
},
recurringEvent: true,
hideCalendarNotes: true,
hideCalendarEventDetails: true,
+60
View File
@@ -0,0 +1,60 @@
import { ReplexicaEngine } from "@replexica/sdk";
import { REPLEXICA_API_KEY } from "@calcom/lib/constants";
export class ReplexicaService {
private static engine = new ReplexicaEngine({
apiKey: REPLEXICA_API_KEY,
});
/**
* Localizes text from one language to another
* @param text The text to localize
* @param sourceLocale The source language locale
* @param targetLocale The target language locale
* @returns The localized text
*/
static async localizeText(text: string, sourceLocale: string, targetLocale: string): Promise<string> {
if (!text?.trim()) {
return text;
}
try {
const result = await this.engine.localizeText(text, {
sourceLocale,
targetLocale,
});
return result;
} catch (error) {
return text;
}
}
/**
* Localizes an array of texts from one language to another
* @param texts Array of texts to localize
* @param sourceLocale The source language locale
* @param targetLocale The target language locale
* @returns The localized texts array
*/
static async localizeTexts(texts: string[], sourceLocale: string, targetLocale: string): Promise<string[]> {
if (!texts.length) {
return texts;
}
try {
const result = await this.engine.localizeChat(
texts.map((text) => ({ name: "NO_NAME", text: text.trim() })),
{
sourceLocale,
targetLocale,
}
);
return result.map((chat: { name: string; text: string }) => chat.text);
} catch (error) {
return texts;
}
}
}
+1
View File
@@ -138,6 +138,7 @@ export const buildEventType = (eventType?: Partial<EventType>): EventType => {
secondaryEmailId: null,
isRRWeightsEnabled: false,
eventTypeColor: null,
autoTranslateDescriptionEnabled: false,
...eventType,
};
};
@@ -109,6 +109,7 @@ export const useEventTypeForm = ({
},
})),
seatsPerTimeSlotEnabled: eventType.seatsPerTimeSlot,
autoTranslateDescriptionEnabled: eventType.autoTranslateDescriptionEnabled,
rescheduleWithSameRoundRobinHost: eventType.rescheduleWithSameRoundRobinHost,
assignAllTeamMembers: eventType.assignAllTeamMembers,
aiPhoneCallConfig: {
@@ -0,0 +1,30 @@
-- CreateEnum
CREATE TYPE "EventTypeAutoTranslatedField" AS ENUM ('DESCRIPTION');
-- AlterTable
ALTER TABLE "EventType" ADD COLUMN "autoTranslateDescriptionEnabled" BOOLEAN NOT NULL DEFAULT false;
-- CreateTable
CREATE TABLE "EventTypeTranslation" (
"id" TEXT NOT NULL,
"eventTypeId" INTEGER NOT NULL,
"field" "EventTypeAutoTranslatedField" NOT NULL,
"sourceLang" TEXT NOT NULL,
"targetLang" TEXT NOT NULL,
"translatedText" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"createdBy" INTEGER NOT NULL,
"updatedAt" TIMESTAMP(3) NOT NULL,
"updatedBy" INTEGER,
CONSTRAINT "EventTypeTranslation_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "EventTypeTranslation_eventTypeId_field_targetLang_idx" ON "EventTypeTranslation"("eventTypeId", "field", "targetLang");
-- CreateIndex
CREATE UNIQUE INDEX "EventTypeTranslation_eventTypeId_field_targetLang_key" ON "EventTypeTranslation"("eventTypeId", "field", "targetLang");
-- AddForeignKey
ALTER TABLE "EventTypeTranslation" ADD CONSTRAINT "EventTypeTranslation_eventTypeId_fkey" FOREIGN KEY ("eventTypeId") REFERENCES "EventType"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+23
View File
@@ -110,6 +110,7 @@ model EventType {
requiresConfirmation Boolean @default(false)
requiresConfirmationWillBlockSlot Boolean @default(false)
requiresBookerEmailVerification Boolean @default(false)
autoTranslateDescriptionEnabled Boolean @default(false)
/// @zod.custom(imports.recurringEventType)
recurringEvent Json?
disableGuests Boolean @default(false)
@@ -149,6 +150,7 @@ model EventType {
useEventTypeDestinationCalendarEmail Boolean @default(false)
aiPhoneCallConfig AIPhoneCallConfiguration?
isRRWeightsEnabled Boolean @default(false)
fieldTranslations EventTypeTranslation[]
maxLeadThreshold Int?
/// @zod.custom(imports.eventTypeColor)
@@ -1590,3 +1592,24 @@ model AttributeToUser {
@@unique([memberId, attributeOptionId])
}
enum EventTypeAutoTranslatedField {
DESCRIPTION // Currently the only field we translate
}
model EventTypeTranslation {
id String @id @default(cuid())
eventType EventType @relation(fields: [eventTypeId], references: [id], onDelete: Cascade)
eventTypeId Int
field EventTypeAutoTranslatedField
sourceLang String
targetLang String
translatedText String @db.Text
createdAt DateTime @default(now())
createdBy Int
updatedAt DateTime @updatedAt
updatedBy Int?
@@unique([eventTypeId, field, targetLang])
@@index([eventTypeId, field, targetLang])
}
@@ -35,6 +35,7 @@ type User = {
id: SessionUser["profile"]["id"] | null;
};
selectedCalendars: SessionUser["selectedCalendars"];
organizationId: number | null;
};
type UpdateOptions = {
@@ -74,6 +75,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
secondaryEmailId,
aiPhoneCallConfig,
isRRWeightsEnabled,
autoTranslateDescriptionEnabled,
...rest
} = input;
@@ -153,6 +155,8 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
const data: Prisma.EventTypeUpdateInput = {
...rest,
// autoTranslate feature is allowed for org users only
autoTranslateDescriptionEnabled: !!(ctx.user.organizationId && autoTranslateDescriptionEnabled),
bookingFields,
isRRWeightsEnabled,
metadata: rest.metadata === null ? Prisma.DbNull : (rest.metadata as Prisma.InputJsonObject),
+444 -5
View File
@@ -5045,6 +5045,7 @@ __metadata:
"@radix-ui/react-switch": ^1.0.0
"@radix-ui/react-toggle-group": ^1.0.0
"@radix-ui/react-tooltip": ^1.0.0
"@replexica/sdk": ^0.6.0
"@sentry/nextjs": ^8.8.0
"@stripe/react-stripe-js": ^1.10.0
"@stripe/stripe-js": ^1.35.0
@@ -9980,7 +9981,7 @@ __metadata:
languageName: node
linkType: hard
"@paralleldrive/cuid2@npm:2.2.2":
"@paralleldrive/cuid2@npm:2.2.2, @paralleldrive/cuid2@npm:^2.2.2":
version: 2.2.2
resolution: "@paralleldrive/cuid2@npm:2.2.2"
dependencies:
@@ -12366,6 +12367,31 @@ __metadata:
languageName: node
linkType: hard
"@replexica/sdk@npm:^0.6.0":
version: 0.6.0
resolution: "@replexica/sdk@npm:0.6.0"
dependencies:
"@paralleldrive/cuid2": ^2.2.2
"@replexica/spec": 0.18.0
jsdom: ^25.0.1
typescript: ^5.6.3
vitest: ^2.1.4
zod: ^3.23.8
checksum: 95a239cf24025a8ea1a03b0720c8a1bd351f73777015fd3a8b74995740fb191c87d157ac04e7b1824f9b5b8f27cef3ce1b7126deb7bf7683b74b8435d3de1238
languageName: node
linkType: hard
"@replexica/spec@npm:0.18.0":
version: 0.18.0
resolution: "@replexica/spec@npm:0.18.0"
dependencies:
typescript: ^5.6.3
vitest: ^2.1.4
zod: ^3.23.8
checksum: d9afcfd965b804d395436cc6daf9a051142e13658297e45f742a6397699360e8f28d43399c4c31077b250ca965e1733200bf8c63eae328b4960320d2dd6ae5b5
languageName: node
linkType: hard
"@resvg/resvg-wasm@npm:2.4.0":
version: 2.4.0
resolution: "@resvg/resvg-wasm@npm:2.4.0"
@@ -17462,6 +17488,18 @@ __metadata:
languageName: node
linkType: hard
"@vitest/expect@npm:2.1.5":
version: 2.1.5
resolution: "@vitest/expect@npm:2.1.5"
dependencies:
"@vitest/spy": 2.1.5
"@vitest/utils": 2.1.5
chai: ^5.1.2
tinyrainbow: ^1.2.0
checksum: feb3dc9397d43a84525ee1966efc940d2175a4ca939b142239a875d43de1efbbfcd347440c2703f74560643c079b0ea1b52a46b80b7e4945f6d5a9ab5762c007
languageName: node
linkType: hard
"@vitest/mocker@npm:2.1.1":
version: 2.1.1
resolution: "@vitest/mocker@npm:2.1.1"
@@ -17482,6 +17520,25 @@ __metadata:
languageName: node
linkType: hard
"@vitest/mocker@npm:2.1.5":
version: 2.1.5
resolution: "@vitest/mocker@npm:2.1.5"
dependencies:
"@vitest/spy": 2.1.5
estree-walker: ^3.0.3
magic-string: ^0.30.12
peerDependencies:
msw: ^2.4.9
vite: ^5.0.0
peerDependenciesMeta:
msw:
optional: true
vite:
optional: true
checksum: 165db0dfdbd78da68be2e880638f5a8ff603745d80acbe0353d4d3c72233d9991545dda746db8c66e592baf9c8d880310681042da9b819affeca9f7a2dd7f389
languageName: node
linkType: hard
"@vitest/pretty-format@npm:2.1.1, @vitest/pretty-format@npm:^2.1.1":
version: 2.1.1
resolution: "@vitest/pretty-format@npm:2.1.1"
@@ -17491,6 +17548,15 @@ __metadata:
languageName: node
linkType: hard
"@vitest/pretty-format@npm:2.1.5, @vitest/pretty-format@npm:^2.1.5":
version: 2.1.5
resolution: "@vitest/pretty-format@npm:2.1.5"
dependencies:
tinyrainbow: ^1.2.0
checksum: 0d470aabe0b27825c19da9e922bd2da073da5a7bdf0db7b101107c83f3071d728d98633b465d73b90c3fd68926f750fbf50d322140853fb30bad51389c766074
languageName: node
linkType: hard
"@vitest/runner@npm:2.1.1":
version: 2.1.1
resolution: "@vitest/runner@npm:2.1.1"
@@ -17501,6 +17567,16 @@ __metadata:
languageName: node
linkType: hard
"@vitest/runner@npm:2.1.5":
version: 2.1.5
resolution: "@vitest/runner@npm:2.1.5"
dependencies:
"@vitest/utils": 2.1.5
pathe: ^1.1.2
checksum: 93ee4c4e23a958a33b0214859f3e5552e2dcaf18625302ac8e824e22ffc4cadb52e6de0b62a1dcfeb7aeae59cde097c21792c9b753f4bde6fbb391f3902e5193
languageName: node
linkType: hard
"@vitest/snapshot@npm:2.1.1":
version: 2.1.1
resolution: "@vitest/snapshot@npm:2.1.1"
@@ -17512,6 +17588,17 @@ __metadata:
languageName: node
linkType: hard
"@vitest/snapshot@npm:2.1.5":
version: 2.1.5
resolution: "@vitest/snapshot@npm:2.1.5"
dependencies:
"@vitest/pretty-format": 2.1.5
magic-string: ^0.30.12
pathe: ^1.1.2
checksum: bb61537367e4f51071234a7917f4c12eb564a243fb9dcd056b44d22cd3b108d2e0da428a8d0fe0363dd12e9b9aeabe155e3d4ae7c849c2e6be932329f6977801
languageName: node
linkType: hard
"@vitest/spy@npm:2.1.1, @vitest/spy@npm:^2.1.0-beta.1":
version: 2.1.1
resolution: "@vitest/spy@npm:2.1.1"
@@ -17521,6 +17608,15 @@ __metadata:
languageName: node
linkType: hard
"@vitest/spy@npm:2.1.5":
version: 2.1.5
resolution: "@vitest/spy@npm:2.1.5"
dependencies:
tinyspy: ^3.0.2
checksum: db5aec91b3ac9130537c0e619940cbb8aebbd7fb2af4da9c42804d0bc7179dec75cd9573b56d8a1ec1c845e5751e3ba1dbc30b07fa38d637f241d77b45c37fbe
languageName: node
linkType: hard
"@vitest/ui@npm:^2.1.1":
version: 2.1.1
resolution: "@vitest/ui@npm:2.1.1"
@@ -17549,6 +17645,17 @@ __metadata:
languageName: node
linkType: hard
"@vitest/utils@npm:2.1.5":
version: 2.1.5
resolution: "@vitest/utils@npm:2.1.5"
dependencies:
"@vitest/pretty-format": 2.1.5
loupe: ^3.1.2
tinyrainbow: ^1.2.0
checksum: f12f6d95d058b537afd9e0b0af1075a80bf77789f32b75bf6a110a2bbf5afd08930ee71a0bd53bfb42bc923e4706d5e26c41df0e1c2854db1a668a011d6c010a
languageName: node
linkType: hard
"@volar/language-core@npm:1.11.1, @volar/language-core@npm:~1.11.1":
version: 1.11.1
resolution: "@volar/language-core@npm:1.11.1"
@@ -20429,6 +20536,19 @@ __metadata:
languageName: node
linkType: hard
"chai@npm:^5.1.2":
version: 5.1.2
resolution: "chai@npm:5.1.2"
dependencies:
assertion-error: ^2.0.1
check-error: ^2.1.1
deep-eql: ^5.0.1
loupe: ^3.1.0
pathval: ^2.0.0
checksum: f2341967ab5632612548d372c27b46219adad3af35021d8cba2ae3c262f588de2c60cb3f004e6ad40e363a9cad6d20d0de51f00e7e9ac31cce17fb05d4efa316
languageName: node
linkType: hard
"chalk@npm:3.0.0, chalk@npm:^3.0.0":
version: 3.0.0
resolution: "chalk@npm:3.0.0"
@@ -22184,6 +22304,15 @@ __metadata:
languageName: node
linkType: hard
"cssstyle@npm:^4.1.0":
version: 4.1.0
resolution: "cssstyle@npm:4.1.0"
dependencies:
rrweb-cssom: ^0.7.1
checksum: a8f5746430c42347e76dc830548f3a296882e42a90af188ae44e4c1a4131aec246b0b6c8562e3e6e4fa0ff14aeee5cd14a0e2fe5a7105dcf39f98eb70d16b634
languageName: node
linkType: hard
"csstype@npm:^3.0.2":
version: 3.1.0
resolution: "csstype@npm:3.1.0"
@@ -22374,6 +22503,16 @@ __metadata:
languageName: node
linkType: hard
"data-urls@npm:^5.0.0":
version: 5.0.0
resolution: "data-urls@npm:5.0.0"
dependencies:
whatwg-mimetype: ^4.0.0
whatwg-url: ^14.0.0
checksum: 5c40568c31b02641a70204ff233bc4e42d33717485d074244a98661e5f2a1e80e38fe05a5755dfaf2ee549f2ab509d6a3af2a85f4b2ad2c984e5d176695eaf46
languageName: node
linkType: hard
"data-view-buffer@npm:^1.0.1":
version: 1.0.1
resolution: "data-view-buffer@npm:1.0.1"
@@ -22498,7 +22637,7 @@ __metadata:
languageName: node
linkType: hard
"debug@npm:^4.3.6":
"debug@npm:^4.3.6, debug@npm:^4.3.7":
version: 4.3.7
resolution: "debug@npm:4.3.7"
dependencies:
@@ -23989,6 +24128,13 @@ __metadata:
languageName: node
linkType: hard
"es-module-lexer@npm:^1.5.4":
version: 1.5.4
resolution: "es-module-lexer@npm:1.5.4"
checksum: a0cf04fb92d052647ac7d818d1913b98d3d3d0f5b9d88f0eafb993436e4c3e2c958599db68839d57f2dfa281fdf0f60e18d448eb78fc292c33c0f25635b6854f
languageName: node
linkType: hard
"es-object-atoms@npm:^1.0.0":
version: 1.0.0
resolution: "es-object-atoms@npm:1.0.0"
@@ -25199,6 +25345,13 @@ __metadata:
languageName: node
linkType: hard
"expect-type@npm:^1.1.0":
version: 1.1.0
resolution: "expect-type@npm:1.1.0"
checksum: 65d25ec10bca32bcf650dcfe734532acc4b7a73677c656f299a7cbed273b5c4d6a3dab11af76f452645d54a95c4ef39fc73772f2c8eb6684ba35672958d6f3b3
languageName: node
linkType: hard
"expect@npm:^29.0.0":
version: 29.6.1
resolution: "expect@npm:29.6.1"
@@ -27674,6 +27827,15 @@ __metadata:
languageName: node
linkType: hard
"html-encoding-sniffer@npm:^4.0.0":
version: 4.0.0
resolution: "html-encoding-sniffer@npm:4.0.0"
dependencies:
whatwg-encoding: ^3.1.1
checksum: 3339b71dab2723f3159a56acf541ae90a408ce2d11169f00fe7e0c4663d31d6398c8a4408b504b4eec157444e47b084df09b3cb039c816660f0dd04846b8957d
languageName: node
linkType: hard
"html-entities@npm:^2.1.0":
version: 2.3.3
resolution: "html-entities@npm:2.3.3"
@@ -27864,7 +28026,7 @@ __metadata:
languageName: node
linkType: hard
"http-proxy-agent@npm:^7.0.0, http-proxy-agent@npm:^7.0.1":
"http-proxy-agent@npm:^7.0.0, http-proxy-agent@npm:^7.0.1, http-proxy-agent@npm:^7.0.2":
version: 7.0.2
resolution: "http-proxy-agent@npm:7.0.2"
dependencies:
@@ -27995,7 +28157,7 @@ __metadata:
languageName: node
linkType: hard
"https-proxy-agent@npm:^7.0.0":
"https-proxy-agent@npm:^7.0.0, https-proxy-agent@npm:^7.0.5":
version: 7.0.5
resolution: "https-proxy-agent@npm:7.0.5"
dependencies:
@@ -30516,6 +30678,40 @@ __metadata:
languageName: node
linkType: hard
"jsdom@npm:^25.0.1":
version: 25.0.1
resolution: "jsdom@npm:25.0.1"
dependencies:
cssstyle: ^4.1.0
data-urls: ^5.0.0
decimal.js: ^10.4.3
form-data: ^4.0.0
html-encoding-sniffer: ^4.0.0
http-proxy-agent: ^7.0.2
https-proxy-agent: ^7.0.5
is-potential-custom-element-name: ^1.0.1
nwsapi: ^2.2.12
parse5: ^7.1.2
rrweb-cssom: ^0.7.1
saxes: ^6.0.0
symbol-tree: ^3.2.4
tough-cookie: ^5.0.0
w3c-xmlserializer: ^5.0.0
webidl-conversions: ^7.0.0
whatwg-encoding: ^3.1.1
whatwg-mimetype: ^4.0.0
whatwg-url: ^14.0.0
ws: ^8.18.0
xml-name-validator: ^5.0.0
peerDependencies:
canvas: ^2.11.2
peerDependenciesMeta:
canvas:
optional: true
checksum: b637d28445d570014195b3c77d06e54ef69d1f807eaf61388cb470e4d9227244e7fe2e0c32b6df03ac4fe35f746d7c721672d9136ecebb49d2e61a04ab9628e0
languageName: node
linkType: hard
"jsesc@npm:^2.5.1":
version: 2.5.2
resolution: "jsesc@npm:2.5.2"
@@ -31753,6 +31949,13 @@ __metadata:
languageName: node
linkType: hard
"loupe@npm:^3.1.2":
version: 3.1.2
resolution: "loupe@npm:3.1.2"
checksum: 4a75bbe8877a1ced3603e08b1095cd6f4c987c50fe63719fdc3009029560f91e07a915e7f6eff1322bb62bfb2a2beeef06b13ccb3c12f81bda9f3674434dcab9
languageName: node
linkType: hard
"lower-case-first@npm:^1.0.0":
version: 1.0.2
resolution: "lower-case-first@npm:1.0.2"
@@ -32006,6 +32209,15 @@ __metadata:
languageName: node
linkType: hard
"magic-string@npm:^0.30.12":
version: 0.30.12
resolution: "magic-string@npm:0.30.12"
dependencies:
"@jridgewell/sourcemap-codec": ^1.5.0
checksum: 3f0d23b74371765f0e6cad4284eebba0ac029c7a55e39292de5aa92281afb827138cb2323d24d2924f6b31f138c3783596c5ccaa98653fe9cf122e1f81325b59
languageName: node
linkType: hard
"mailhog@npm:^4.16.0":
version: 4.16.0
resolution: "mailhog@npm:4.16.0"
@@ -34148,6 +34360,13 @@ __metadata:
languageName: node
linkType: hard
"nwsapi@npm:^2.2.12":
version: 2.2.13
resolution: "nwsapi@npm:2.2.13"
checksum: d34fb7838517c3c7e8cc824e443275b08b57f6a025a860693d18c56ddcfd176e32df9bf0ae7f5a95c7a32981501caa1f9fda31b59f28aa72a4b9d01f573a8e6b
languageName: node
linkType: hard
"nwsapi@npm:^2.2.4":
version: 2.2.4
resolution: "nwsapi@npm:2.2.4"
@@ -36840,6 +37059,13 @@ __metadata:
languageName: node
linkType: hard
"punycode@npm:^2.3.1":
version: 2.3.1
resolution: "punycode@npm:2.3.1"
checksum: bb0a0ceedca4c3c57a9b981b90601579058903c62be23c5e8e843d2c2d4148a3ecf029d5133486fb0e1822b098ba8bba09e89d6b21742d02fa26bda6441a6fb2
languageName: node
linkType: hard
"pupa@npm:^3.1.0":
version: 3.1.0
resolution: "pupa@npm:3.1.0"
@@ -39303,6 +39529,13 @@ __metadata:
languageName: node
linkType: hard
"rrweb-cssom@npm:^0.7.1":
version: 0.7.1
resolution: "rrweb-cssom@npm:0.7.1"
checksum: 62e410ddbaaba6abc196c3bbfa8de4952e0a134d9f2b454ee293039bf9931322d806e14d52ed122a5c2bd332a868b9da2e99358fb6232c33758b5ede86d992c8
languageName: node
linkType: hard
"rtl-css-js@npm:^1.16.1":
version: 1.16.1
resolution: "rtl-css-js@npm:1.16.1"
@@ -40611,6 +40844,13 @@ __metadata:
languageName: node
linkType: hard
"std-env@npm:^3.8.0":
version: 3.8.0
resolution: "std-env@npm:3.8.0"
checksum: ad4554485c2d09138a1d0f03944245e169510e6f5200b7d30fcdd4536e27a2a9a2fd934caff7ef58ebbe21993fa0e2b9e5b1979f431743c925305863b7ff36d5
languageName: node
linkType: hard
"stdin-discarder@npm:^0.2.1":
version: 0.2.2
resolution: "stdin-discarder@npm:0.2.2"
@@ -42119,6 +42359,13 @@ __metadata:
languageName: node
linkType: hard
"tinyexec@npm:^0.3.1":
version: 0.3.1
resolution: "tinyexec@npm:0.3.1"
checksum: 691b531d464bdc09eeba934e43d8ac2a74c9d22a4bec9cd7f4991375c64e22712f7e5a95ba243a9369a478afd34d41171359012a2248ea49615cd2816ab12959
languageName: node
linkType: hard
"tinyglobby@npm:^0.2.6":
version: 0.2.6
resolution: "tinyglobby@npm:0.2.6"
@@ -42146,6 +42393,13 @@ __metadata:
languageName: node
linkType: hard
"tinypool@npm:^1.0.1":
version: 1.0.2
resolution: "tinypool@npm:1.0.2"
checksum: 752f23114d8fc95a9497fc812231d6d0a63728376aa11e6e8499c10423a91112e760e388887ea7854f1b16977c321f07c0eab061ec2f60f6761e58b184aac880
languageName: node
linkType: hard
"tinyrainbow@npm:^1.2.0":
version: 1.2.0
resolution: "tinyrainbow@npm:1.2.0"
@@ -42153,7 +42407,7 @@ __metadata:
languageName: node
linkType: hard
"tinyspy@npm:^3.0.0":
"tinyspy@npm:^3.0.0, tinyspy@npm:^3.0.2":
version: 3.0.2
resolution: "tinyspy@npm:3.0.2"
checksum: 5db671b2ff5cd309de650c8c4761ca945459d7204afb1776db9a04fb4efa28a75f08517a8620c01ee32a577748802231ad92f7d5b194dc003ee7f987a2a06337
@@ -42170,6 +42424,24 @@ __metadata:
languageName: node
linkType: hard
"tldts-core@npm:^6.1.61":
version: 6.1.61
resolution: "tldts-core@npm:6.1.61"
checksum: 659411060be80658bd492cf0f3a38223c0cc15e2ffb402e219e51dcfcffcdd581c00e63e494944d4e7ff0a0aeb321058f7811d9089fac455ae0c10b4f5beffd1
languageName: node
linkType: hard
"tldts@npm:^6.1.32":
version: 6.1.61
resolution: "tldts@npm:6.1.61"
dependencies:
tldts-core: ^6.1.61
bin:
tldts: bin/cli.js
checksum: 1d2feafe6160147a2f5ea520ebc84c77153edacd0d69510726ee0c13b2fce8b84d1bad79f2c5337ac361d297d0d16815fe5d3c51c88a7822d2320a75f6d2dea3
languageName: node
linkType: hard
"tmp@npm:0.2.1":
version: 0.2.1
resolution: "tmp@npm:0.2.1"
@@ -42284,6 +42556,15 @@ __metadata:
languageName: node
linkType: hard
"tough-cookie@npm:^5.0.0":
version: 5.0.0
resolution: "tough-cookie@npm:5.0.0"
dependencies:
tldts: ^6.1.32
checksum: 774f6c939c96f74b5847361f7e11e0d69383681d21a35a2d37a20956638e614ec521782d2d20bcb32b58638ff337bba87cc72fb72c987bd02ea0fdfc93994cdb
languageName: node
linkType: hard
"tough-cookie@npm:~2.5.0":
version: 2.5.0
resolution: "tough-cookie@npm:2.5.0"
@@ -42303,6 +42584,15 @@ __metadata:
languageName: node
linkType: hard
"tr46@npm:^5.0.0":
version: 5.0.0
resolution: "tr46@npm:5.0.0"
dependencies:
punycode: ^2.3.1
checksum: 8d8b021f8e17675ebf9e672c224b6b6cfdb0d5b92141349e9665c14a2501c54a298d11264bbb0b17b447581e1e83d4fc3c038c929f3d210e3964d4be47460288
languageName: node
linkType: hard
"tr46@npm:~0.0.3":
version: 0.0.3
resolution: "tr46@npm:0.0.3"
@@ -43215,6 +43505,16 @@ __metadata:
languageName: node
linkType: hard
"typescript@npm:^5.6.3":
version: 5.6.3
resolution: "typescript@npm:5.6.3"
bin:
tsc: bin/tsc
tsserver: bin/tsserver
checksum: ba302f8822777ebefb28b554105f3e074466b671e7444ec6b75dadc008a62f46f373d9e57ceced1c433756d06c8b7dc569a7eefdf3a9573122a49205ff99021a
languageName: node
linkType: hard
"typescript@patch:typescript@5.3.3#~builtin<compat/typescript>":
version: 5.3.3
resolution: "typescript@patch:typescript@npm%3A5.3.3#~builtin<compat/typescript>::version=5.3.3&hash=1f5320"
@@ -43245,6 +43545,16 @@ __metadata:
languageName: node
linkType: hard
"typescript@patch:typescript@^5.6.3#~builtin<compat/typescript>":
version: 5.6.3
resolution: "typescript@patch:typescript@npm%3A5.6.3#~builtin<compat/typescript>::version=5.6.3&hash=1f5320"
bin:
tsc: bin/tsc
tsserver: bin/tsserver
checksum: ade87bce2363ee963eed0e4ca8a312ea02c81873ebd53609bc3f6dc0a57f6e61ad7e3fb8cbb7f7ab8b5081cbee801b023f7c4823ee70b1c447eae050e6c7622b
languageName: node
linkType: hard
"tzdata@npm:^1.0.30":
version: 1.0.40
resolution: "tzdata@npm:1.0.40"
@@ -43901,6 +44211,21 @@ __metadata:
languageName: node
linkType: hard
"vite-node@npm:2.1.5":
version: 2.1.5
resolution: "vite-node@npm:2.1.5"
dependencies:
cac: ^6.7.14
debug: ^4.3.7
es-module-lexer: ^1.5.4
pathe: ^1.1.2
vite: ^5.0.0
bin:
vite-node: vite-node.mjs
checksum: b7c12c330f0cf5bb63fd6d36c38d7f1f9ed5cea5a7407db626945038c66e3e4165f79bdbcb928c6a289f299f434fb0a30bd1ab5c9c0d50eb892164bcdc37d167
languageName: node
linkType: hard
"vite-plugin-dts@npm:^3.7.2, vite-plugin-dts@npm:^3.7.3":
version: 3.7.3
resolution: "vite-plugin-dts@npm:3.7.3"
@@ -44199,6 +44524,56 @@ __metadata:
languageName: node
linkType: hard
"vitest@npm:^2.1.4":
version: 2.1.5
resolution: "vitest@npm:2.1.5"
dependencies:
"@vitest/expect": 2.1.5
"@vitest/mocker": 2.1.5
"@vitest/pretty-format": ^2.1.5
"@vitest/runner": 2.1.5
"@vitest/snapshot": 2.1.5
"@vitest/spy": 2.1.5
"@vitest/utils": 2.1.5
chai: ^5.1.2
debug: ^4.3.7
expect-type: ^1.1.0
magic-string: ^0.30.12
pathe: ^1.1.2
std-env: ^3.8.0
tinybench: ^2.9.0
tinyexec: ^0.3.1
tinypool: ^1.0.1
tinyrainbow: ^1.2.0
vite: ^5.0.0
vite-node: 2.1.5
why-is-node-running: ^2.3.0
peerDependencies:
"@edge-runtime/vm": "*"
"@types/node": ^18.0.0 || >=20.0.0
"@vitest/browser": 2.1.5
"@vitest/ui": 2.1.5
happy-dom: "*"
jsdom: "*"
peerDependenciesMeta:
"@edge-runtime/vm":
optional: true
"@types/node":
optional: true
"@vitest/browser":
optional: true
"@vitest/ui":
optional: true
happy-dom:
optional: true
jsdom:
optional: true
bin:
vitest: vitest.mjs
checksum: 1a7ddc055aae55452d7fce86cd9aeb92dcdcc3d51105dd87c34c2c0ac2c508c3e20be36c855568686fe19fb06e9f2568fca511ae70511a782155a3449dbdc083
languageName: node
linkType: hard
"vm-browserify@npm:^1.0.1, vm-browserify@npm:^1.1.2":
version: 1.1.2
resolution: "vm-browserify@npm:1.1.2"
@@ -44247,6 +44622,15 @@ __metadata:
languageName: node
linkType: hard
"w3c-xmlserializer@npm:^5.0.0":
version: 5.0.0
resolution: "w3c-xmlserializer@npm:5.0.0"
dependencies:
xml-name-validator: ^5.0.0
checksum: 593acc1fdab3f3207ec39d851e6df0f3fa41a36b5809b0ace364c7a6d92e351938c53424a7618ce8e0fbaffee8be2e8e070a5734d05ee54666a8bdf1a376cc40
languageName: node
linkType: hard
"walker@npm:^1.0.8":
version: 1.0.8
resolution: "walker@npm:1.0.8"
@@ -44519,6 +44903,15 @@ __metadata:
languageName: node
linkType: hard
"whatwg-encoding@npm:^3.1.1":
version: 3.1.1
resolution: "whatwg-encoding@npm:3.1.1"
dependencies:
iconv-lite: 0.6.3
checksum: f75a61422421d991e4aec775645705beaf99a16a88294d68404866f65e92441698a4f5b9fa11dd609017b132d7b286c3c1534e2de5b3e800333856325b549e3c
languageName: node
linkType: hard
"whatwg-fetch@npm:^3.4.1, whatwg-fetch@npm:^3.6.2":
version: 3.6.20
resolution: "whatwg-fetch@npm:3.6.20"
@@ -44533,6 +44926,13 @@ __metadata:
languageName: node
linkType: hard
"whatwg-mimetype@npm:^4.0.0":
version: 4.0.0
resolution: "whatwg-mimetype@npm:4.0.0"
checksum: f97edd4b4ee7e46a379f3fb0e745de29fe8b839307cc774300fd49059fcdd560d38cb8fe21eae5575b8f39b022f23477cc66e40b0355c2851ce84760339cef30
languageName: node
linkType: hard
"whatwg-url@npm:^12.0.0, whatwg-url@npm:^12.0.1":
version: 12.0.1
resolution: "whatwg-url@npm:12.0.1"
@@ -44553,6 +44953,16 @@ __metadata:
languageName: node
linkType: hard
"whatwg-url@npm:^14.0.0":
version: 14.0.0
resolution: "whatwg-url@npm:14.0.0"
dependencies:
tr46: ^5.0.0
webidl-conversions: ^7.0.0
checksum: 4b5887e50f786583bead70916413e67a381d2126899b9eb5c67ce664bba1e7ec07cdff791404581ce73c6190d83c359c9ca1d50711631217905db3877dec075c
languageName: node
linkType: hard
"whatwg-url@npm:^5.0.0":
version: 5.0.0
resolution: "whatwg-url@npm:5.0.0"
@@ -44906,6 +45316,21 @@ __metadata:
languageName: node
linkType: hard
"ws@npm:^8.18.0":
version: 8.18.0
resolution: "ws@npm:8.18.0"
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: ">=5.0.2"
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
checksum: 91d4d35bc99ff6df483bdf029b9ea4bfd7af1f16fc91231a96777a63d263e1eabf486e13a2353970efc534f9faa43bdbf9ee76525af22f4752cbc5ebda333975
languageName: node
linkType: hard
"xdg-basedir@npm:^5.0.1, xdg-basedir@npm:^5.1.0":
version: 5.1.0
resolution: "xdg-basedir@npm:5.1.0"
@@ -44974,6 +45399,13 @@ __metadata:
languageName: node
linkType: hard
"xml-name-validator@npm:^5.0.0":
version: 5.0.0
resolution: "xml-name-validator@npm:5.0.0"
checksum: 86effcc7026f437701252fcc308b877b4bc045989049cfc79b0cc112cb365cf7b009f4041fab9fb7cd1795498722c3e9fe9651afc66dfa794c16628a639a4c45
languageName: node
linkType: hard
"xml-parse-from-string@npm:^1.0.0":
version: 1.0.1
resolution: "xml-parse-from-string@npm:1.0.1"
@@ -45386,6 +45818,13 @@ __metadata:
languageName: node
linkType: hard
"zod@npm:^3.23.8":
version: 3.23.8
resolution: "zod@npm:3.23.8"
checksum: 15949ff82118f59c893dacd9d3c766d02b6fa2e71cf474d5aa888570c469dbf5446ac5ad562bb035bf7ac9650da94f290655c194f4a6de3e766f43febd432c5c
languageName: node
linkType: hard
"zustand@npm:^4.3.2":
version: 4.5.2
resolution: "zustand@npm:4.5.2"