diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bff4242..6159584 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: - node-version: [16.x, 18.x, 20.x] + node-version: [20.x, 22.x] steps: - uses: actions/checkout@v4 @@ -22,12 +22,7 @@ jobs: node-version: ${{ matrix.node-version }} cache: 'yarn' - - name: Install dependencies (Node 16.x) - if: matrix.node-version == '16.x' - run: yarn install --frozen-lockfile --ignore-engines - - - name: Install dependencies (Node 18.x and 20.x) - if: matrix.node-version != '16.x' + - name: Install dependencies run: yarn install --frozen-lockfile - name: Check format diff --git a/.gitignore b/.gitignore index 07d77d6..0c157f6 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,10 @@ /next-example .idea -coverage \ No newline at end of file +coverage +.claude/ +.DS_Store +AUDIT_REPORT.md +COMPLETION_SUMMARY.md +FIXES_IMPLEMENTED.md +package-lock.json diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3a50fbd --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,16 @@ +# Changelog + +## 1.5.0 (unreleased) + +### Changed + +- Failed SDK init surfaces as an error state; single ready callback per transition. +- Attributes set before the first assignment; peek during render. + +### Added + +- SSR support for Treatment components and `useTreatment`. + +### Fixed + +- Error boundary, SDK provider and treatment components; context created before finalizing the old one. diff --git a/README.md b/README.md index a37bd37..0bf76eb 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,872 @@ -# A/B Smartly React SDK [![npm version](https://badge.fury.io/js/%40absmartly%2Freact-sdk.svg)](https://badge.fury.io/js/%40absmartly%2Freact-sdk) +# ABsmartly React SDK [![npm version](https://badge.fury.io/js/%40absmartly%2Freact-sdk.svg)](https://badge.fury.io/js/%40absmartly%2Freact-sdk) -A/B Smartly - React SDK +A/B Smartly - React SDK. This package provides React-specific hooks, components, and providers for the [A/B Smartly](https://www.absmartly.com/) A/B testing platform, built on top of the [A/B Smartly JavaScript SDK](https://www.github.com/absmartly/javascript-sdk). ## Compatibility -The A/B Smartly React SDK is a thin wrapper around the [A/B Smartly JavaScript SDK](https://www.github.com/absmartly/javascript-sdk) +The A/B Smartly React SDK is compatible with: -## Installation +- **React**: 16.8+ (requires hooks support) +- **Rendering**: Client-side and server-side rendering (SSR) + +**Browser Support:** + +- Modern browsers (Chrome, Firefox, Safari, Edge) +- IE 11+ (requires Promise polyfill) + +**Note**: IE 10-11 do not natively support Promises. Include a polyfill like [es6-promise](https://www.npmjs.com/package/es6-promise) or [rsvp](https://www.npmjs.com/package/rsvp). + +### Security Warning + +**IMPORTANT:** The API key is exposed in your client-side bundle and can be viewed by anyone inspecting network traffic or JavaScript source. + +**Best Practices:** + +- Only use **publishable/public keys** with **read-only permissions** in browser applications +- **NEVER** use secret keys with write permissions in client-side code +- For write operations or sensitive API calls, implement a **server-side proxy**: + ```text + Browser --> Your Backend (with secret key) --> ABSmartly API + ``` -#### npm +## Installation ```shell npm install @absmartly/react-sdk --save ``` -## Documentation +## Getting Started + +### Recommended: Using SDKProvider + +Wrap your application with the `SDKProvider` component to make the SDK available throughout your React component tree. + +```jsx +import React from "react"; +import ReactDOM from "react-dom"; +import SDKProvider from "@absmartly/react-sdk"; +import App from "./App"; + +const sdkOptions = { + endpoint: "https://your-company.absmartly.io/v1", + apiKey: process.env.REACT_APP_ABSMARTLY_API_KEY, + environment: process.env.NODE_ENV, + application: "website", +}; + +const contextOptions = { + units: { + session_id: "5ebf06d8cb5d8137290c4abb64155584fbdb64d8", + }, +}; + +ReactDOM.render( + + + , + document.getElementById("root"), +); +``` + +### Alternative: Using a Pre-initialized Context + +For SSR or when you need more control, you can create the context beforehand and pass it to the provider. + +```jsx +import React from "react"; +import SDKProvider, { SDK } from "@absmartly/react-sdk"; +import App from "./App"; + +const sdk = new SDK({ + endpoint: "https://your-company.absmartly.io/v1", + apiKey: process.env.REACT_APP_ABSMARTLY_API_KEY, + environment: process.env.NODE_ENV, + application: "website", +}); + +const context = sdk.createContext({ + units: { + session_id: "5ebf06d8cb5d8137290c4abb64155584fbdb64d8", + }, +}); + +function AppWrapper() { + return ( + + + + ); +} +``` + +**SDK Options** + +| Config | Type | Required? | Default | Description | +| :---------- | :----------------------------------- | :-------: | :-----: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| endpoint | `string` | ✅ | `null` | The URL to your API endpoint. Most commonly `"your-company.absmartly.io"` | +| apiKey | `string` | ✅ | `null` | Your API key which can be found on the Web Console. | +| environment | `string` | ✅ | `null` | The environment of the platform where the SDK is installed. Environments are created on the Web Console and should match the available environments in your infrastructure. | +| application | `string` | ✅ | `null` | The name of the application where the SDK is installed. Applications are created on the Web Console and should match the applications where your experiments will be running. | +| retries | `number` | ❌ | `5` | Number of retry attempts for failed HTTP requests. | +| timeout | `number` | ❌ | `3000` | Connection timeout in milliseconds. | +| eventLogger | `(context, eventName, data) => void` | ❌ | `null` | Callback to handle SDK events (ready, exposure, goal, etc.) | + +**Context Options** + +| Config | Type | Required? | Default | Description | +| :------------ | :------- | :-------: | :-----: | :------------------------------------------------------------------ | +| units | `object` | ✅ | `{}` | Map of unit types to unit identifiers (e.g., `{session_id: "..."}`) | +| publishDelay | `number` | ❌ | `100` | Delay in milliseconds before publishing events. | +| refreshPeriod | `number` | ❌ | `0` | Interval in milliseconds to refresh experiment data. | + +## Creating a New Context + +### With SDKProvider (Automatic) + +When using `SDKProvider` with `sdkOptions` and `contextOptions`, the context is created and managed automatically. The context becomes available to all child components through hooks. + +```jsx + + + +``` + +### With Pre-fetched Data + +When doing full-stack experimentation with A/B Smartly, we recommend creating a context only once on the server-side. Creating a context involves a round-trip to the A/B Smartly event collector. You can avoid repeating the round-trip on the client-side by passing the server-side data directly. + +```jsx +const sdk = new SDK({ + endpoint: "https://your-company.absmartly.io/v1", + apiKey: process.env.REACT_APP_ABSMARTLY_API_KEY, + environment: process.env.NODE_ENV, + application: "website", +}); + +const context = sdk.createContextWith( + { units: { session_id: sessionId } }, + serverSideContextData, +); + +function App() { + return ( + + + + ); +} +``` + +### Refreshing the Context with Fresh Experiment Data + +For long-running single-page applications (SPA), the context is usually created once when the application is first reached. However, any experiments being tracked in your production code, but started after the context was created, will not be triggered. To mitigate this, use the `refreshPeriod` option. + +```jsx +const contextOptions = { + units: { + session_id: "5ebf06d8cb5d8137290c4abb64155584fbdb64d8", + }, + refreshPeriod: 5 * 60 * 1000, // 5 minutes +}; +``` + +Alternatively, the `refresh()` method can be called manually: + +```jsx +function RefreshButton() { + const { context } = useABSmartly(); + + const handleRefresh = async () => { + try { + await context.refresh(); + console.log("Context refreshed with latest experiment data"); + } catch (error) { + console.error("Failed to refresh context:", error); + } + }; + + return ; +} +``` + +### Setting Extra Units + +You can add additional units to a context by calling the `unit()` or `units()` method. This is useful when a user logs in to your application and you want to add the new unit type to the context. + +> **Note:** You cannot override an already set unit type as that would be a change of identity. In this case, use the `resetContext` method to create a new context. + +The `unit()` and `units()` methods can be called before the context is ready. + +```jsx +function UserProfile() { + const { context } = useABSmartly(); + + const handleLogin = (userId) => { + context.unit("user_id", userId); + + // or multiple units + context.units({ + user_id: userId, + db_user_id: "1000013", + }); + }; + + return ; +} +``` + +## Basic Usage + +### Using the useTreatment Hook + +The `useTreatment` hook is the primary way to access experiment treatments in functional components. + +```jsx +import React from "react"; +import { useTreatment } from "@absmartly/react-sdk"; + +function MyButton() { + const { variant, loading, error } = useTreatment("exp_button_color"); + + if (loading) return ; + if (error) return ; + + return ( + + ); +} +``` + +### Using the Treatment Component + +For declarative treatment rendering, use the `Treatment` component with `TreatmentVariant` children. Variants can be specified as numbers or letters (`"A"`, `"B"`, `"C"`). + +```jsx +import React from "react"; +import { Treatment, TreatmentVariant } from "@absmartly/react-sdk"; + +function MyFeature() { + return ( + + +
Control - Old Feature
+
+ +
Treatment - New Feature
+
+
+ ); +} +``` + +You can provide a loading component to show while the context is being fetched: + +```jsx +Loading...}> + +
Control
+
+ +
Treatment
+
+
+``` + +### Using the TreatmentFunction Component + +The `TreatmentFunction` component provides the variant number and all experiment variables to a render function, giving you more flexibility. + +```jsx +import React from "react"; +import { TreatmentFunction } from "@absmartly/react-sdk"; + +function PricingExperiment() { + return ( + + {({ variant, variables }) => ( +
+

Variant: {variant}

+

Price: {variables.price || "$9.99"}

+
+ )} +
+ ); +} +``` + +### Using the useABSmartly Hook + +For direct access to the SDK and context: + +```jsx +import React from "react"; +import { useABSmartly } from "@absmartly/react-sdk"; + +function MyComponent() { + const { context, sdk } = useABSmartly(); + + const handleClick = () => { + const treatment = context.treatment("exp_test_experiment"); + console.log("Current treatment:", treatment); + + context.track("button_clicked", { + timestamp: Date.now(), + }); + }; + + return ; +} +``` + +### Treatment Variables + +Access variable values associated with treatments: + +```jsx +import React from "react"; +import { useABSmartly } from "@absmartly/react-sdk"; + +function ThemedButton() { + const { context } = useABSmartly(); + + React.useEffect(() => { + context.ready().then(() => { + const buttonColor = context.variableValue("button.color", "red"); + const buttonText = context.variableValue("button.text", "Click Me"); + console.log("Button config:", { buttonColor, buttonText }); + }); + }, [context]); + + return ; +} +``` + +### Peek at Treatment Variants + +Check a treatment without triggering an exposure event. This is useful for analytics or logging purposes where you need the variant but don't want to count it as an actual exposure. + +Using the `useTreatment` hook with `peek = true`: + +```jsx +import React from "react"; +import { useTreatment } from "@absmartly/react-sdk"; + +function FeaturePreview() { + const { variant } = useTreatment("exp_test_experiment", true); + + return
Preview variant: {variant}
; +} +``` + +Or with the context directly: + +```jsx +import React from "react"; +import { useABSmartly } from "@absmartly/react-sdk"; + +function FeatureCheck() { + const { context } = useABSmartly(); + const [peekedVariant, setPeekedVariant] = React.useState(null); + + React.useEffect(() => { + context.ready().then(() => { + const variant = context.peek("exp_test_experiment"); + setPeekedVariant(variant); + }); + }, [context]); + + return
Variant (no exposure): {peekedVariant}
; +} +``` + +### Overriding Treatment Variants + +During development, it is useful to force a treatment for an experiment. This can be achieved with the `override()` and/or `overrides()` methods. These methods can be called before the context is ready. + +```jsx +import React from "react"; +import { useABSmartly } from "@absmartly/react-sdk"; + +function DevTools() { + const { context } = useABSmartly(); + + React.useEffect(() => { + if (process.env.NODE_ENV === "development") { + context.override("exp_test_experiment", 1); + + context.overrides({ + exp_test_experiment: 1, + exp_another_experiment: 0, + }); + } + }, [context]); + + return
Dev Tools Active
; +} +``` + +## Advanced + +### Context Attributes + +Attributes are used to pass meta-data about the user and/or the request. They can be used later in the Web Console to create segments or audiences. They can be set using the `attribute()` or `attributes()` methods, before or after the context is ready. + +```jsx +import React from "react"; +import { useABSmartly } from "@absmartly/react-sdk"; + +function App() { + const { context } = useABSmartly(); + + React.useEffect(() => { + context.attribute("user_agent", navigator.userAgent); + + context.attributes({ + customer_age: "new_customer", + subscription_tier: "premium", + }); + }, [context]); + + return
App Content
; +} +``` + +### Custom Assignments + +Sometimes it may be necessary to override the automatic selection of a variant. For example, if you wish to have your variant chosen based on data from an API call. This can be accomplished using the `customAssignment()` method. + +```jsx +context.customAssignment("exp_test_experiment", 1); + +context.customAssignments({ + exp_test_experiment: 1, +}); +``` + +### Tracking Goals + +Goals are created in the A/B Smartly web console. + +```jsx +import React from "react"; +import { useABSmartly } from "@absmartly/react-sdk"; + +function CheckoutButton() { + const { context } = useABSmartly(); + + const handleCheckout = () => { + context.track("payment", { + item_count: 1, + total_amount: 1999.99, + currency: "USD", + }); + }; + + return ; +} +``` + +### Publishing Pending Data + +Sometimes it is necessary to ensure all events have been published to the A/B Smartly collector, before proceeding. One such case is when the user is about to navigate away right before being exposed to a treatment. You can explicitly call the `publish()` method, which returns a promise. + +```jsx +import React from "react"; +import { useABSmartly } from "@absmartly/react-sdk"; +import { useNavigate } from "react-router-dom"; + +function NavigateAway() { + const { context } = useABSmartly(); + const navigate = useNavigate(); + + const handleNavigate = async () => { + await context.publish(); + navigate("/next-page"); + }; + + return ; +} +``` + +### Finalizing the Context + +The `finalize()` method will ensure all events have been published to the A/B Smartly collector, like `publish()`, and will also "seal" the context, throwing an error if any method that could generate an event is called. + +```jsx +import React from "react"; +import { useABSmartly } from "@absmartly/react-sdk"; + +function Logout() { + const { context } = useABSmartly(); + + const handleLogout = async () => { + await context.finalize(); + window.location.href = "/login"; + }; + + return ; +} +``` + +### Resetting the Context + +When a user's identity changes (e.g., login/logout), reset the context with new units. The `resetContext` method finalizes the old context and creates a new one with pre-fetched data to avoid a round-trip. + +```jsx +import React from "react"; +import { useABSmartly } from "@absmartly/react-sdk"; + +function LoginForm() { + const { resetContext } = useABSmartly(); + + const handleLogin = async (userId) => { + await resetContext({ units: { user_id: userId } }, { publishDelay: 100 }); + }; + + return ( +
{ + e.preventDefault(); + handleLogin("new-user-id"); + }} + > + +
+ ); +} +``` + +### Using a Custom Event Logger + +The A/B Smartly SDK can be instantiated with an event logger used for all contexts. In addition, an event logger can be specified when creating a particular context. + +```jsx +import React from "react"; +import SDKProvider from "@absmartly/react-sdk"; + +const customEventLogger = (context, eventName, data) => { + switch (eventName) { + case "exposure": + console.log("User exposed to experiment:", data.name); + break; + case "goal": + console.log("Goal tracked:", data.name); + break; + case "error": + console.error("SDK error:", data); + break; + case "ready": + console.log("Context ready"); + break; + case "refresh": + console.log("Context refreshed"); + break; + case "publish": + console.log("Events published"); + break; + case "finalize": + console.log("Context finalized"); + break; + } +}; + +function App() { + const sdkOptions = { + endpoint: "https://your-company.absmartly.io/v1", + apiKey: process.env.REACT_APP_ABSMARTLY_API_KEY, + environment: process.env.NODE_ENV, + application: "website", + eventLogger: customEventLogger, + }; + + return ( + + {/* Your app */} + + ); +} +``` + +**Event Types** + +| Event | When | Data | +| :----------- | :-------------------------------------------------- | :------------------------------------------- | +| `"error"` | Context receives an error | Error object thrown | +| `"ready"` | Context turns ready | Data used to initialize the context | +| `"refresh"` | `context.refresh()` method succeeds | Data used to refresh the context | +| `"publish"` | `context.publish()` method succeeds | Data sent to the A/B Smartly event collector | +| `"exposure"` | `context.treatment()` succeeds on first exposure | Exposure data enqueued for publishing | +| `"goal"` | `context.track()` method succeeds | Goal data enqueued for publishing | +| `"finalize"` | `context.finalize()` method succeeds the first time | undefined | + +## React-Specific Patterns + +### Using with TypeScript + +The React SDK exports TypeScript types for all public APIs. + +```tsx +import React from "react"; +import SDKProvider, { useABSmartly, useTreatment } from "@absmartly/react-sdk"; +import type { SDKOptionsType, ContextOptionsType } from "@absmartly/react-sdk"; + +const sdkOptions: SDKOptionsType = { + endpoint: "https://your-company.absmartly.io/v1", + apiKey: process.env.REACT_APP_ABSMARTLY_API_KEY!, + environment: process.env.NODE_ENV, + application: "website", +}; + +const contextOptions: ContextOptionsType = { + units: { + session_id: "5ebf06d8cb5d8137290c4abb64155584fbdb64d8", + }, +}; + +function MyComponent(): JSX.Element { + const { variant, loading, error } = useTreatment("exp_test"); + + if (loading) return
Loading...
; + if (error) return
Error: {error.message}
; + + return
Variant: {variant}
; +} +``` + +### Server-Side Rendering (SSR) + +For Next.js or other SSR frameworks, create the context on the server and pass it to the provider: + +```jsx +// pages/_app.js +import SDKProvider, { SDK } from "@absmartly/react-sdk"; + +function MyApp({ Component, pageProps, absmartlyContext }) { + return ( + + + + ); +} + +MyApp.getInitialProps = async (appContext) => { + const sdk = new SDK({ + endpoint: "https://your-company.absmartly.io/v1", + apiKey: process.env.ABSMARTLY_API_KEY, + environment: process.env.NODE_ENV, + application: "website", + }); + + const context = sdk.createContext({ + units: { + session_id: appContext.ctx.req?.cookies?.session_id || "guest", + }, + }); + + await context.ready(); + + return { + absmartlyContext: context, + }; +}; + +export default MyApp; +``` + +### Higher-Order Component (HOC) + +For class components that cannot use hooks, use the `withABSmartly` HOC: + +```jsx +import React from "react"; +import { withABSmartly } from "@absmartly/react-sdk"; + +class MyComponent extends React.Component { + componentDidMount() { + const { absmartly } = this.props; + absmartly.context.ready().then(() => { + const treatment = absmartly.context.treatment("exp_test"); + console.log("Treatment:", treatment); + }); + } + + render() { + return
My Component
; + } +} + +export default withABSmartly(MyComponent); +``` + +The HOC injects an `absmartly` prop with the following shape: + +```typescript +{ + sdk: ABSmartlySDK; + context: ABSmartlyContext; + resetContext: (params, options?) => Promise; +} +``` + +## API Reference + +### Hooks + +#### `useTreatment(name: string, peek?: boolean)` + +Gets the treatment variant for an experiment. + +**Parameters:** -Documentation for the A/B Smartly React SDK can be found on [our docs site](https://docs.absmartly.com/docs/SDK%20Documentation/getting-started) +- `name` - Experiment name +- `peek` - If true, does not trigger an exposure event (default: `false`) -#### Further info +**Returns:** + +```typescript +{ + variant: number | null; + loading: boolean; + error: Error | null; + context: ABSmartlyContext; +} +``` + +#### `useABSmartly()` + +Returns the SDK instance, context, and reset function. Throws if used outside of `SDKProvider`. + +**Returns:** + +```typescript +{ + sdk: ABSmartlySDK; + context: ABSmartlyContext; + resetContext: (params: ContextRequestType, options?: ContextOptionsType) => Promise; + contextError?: Error | null; +} +``` + +#### `useContextReady(options)` + +Low-level hook for waiting on context readiness. Used internally by `Treatment` and `TreatmentFunction`. + +**Options:** + +```typescript +{ + context: Context; + name: string; + attributes?: Record; + onReady?: (context: Context) => void; +} +``` + +**Returns:** + +```typescript +{ + loading: boolean; + error: Error | null; +} +``` -Please refer to the [A/B Smartly JavaScript SDK](https://www.github.com/absmartly/javascript-sdk) for more details. +### Components + +#### `` + +Provider component that initializes the SDK and makes it available to child components. + +**Props (option 1 - automatic context creation):** + +```typescript +{ + sdkOptions: SDKOptionsType; + contextOptions: { units: Record }; + children?: ReactNode; +} +``` + +**Props (option 2 - pre-initialized context):** + +```typescript +{ + context: ABSmartlyContext; + children?: ReactNode; +} +``` + +#### `` + +Declarative component for rendering different treatment variants. + +**Props:** + +```typescript +{ + name: string; + context?: Context; + attributes?: Record; + loadingComponent?: ReactNode; + children?: ReactNode; +} +``` + +#### `` + +Child component of `` that defines a specific variant. + +**Props:** + +```typescript +{ + variant: number | string; + children?: ReactNode; +} +``` + +#### `` + +Render-prop component that provides variant and variables to a render function. + +**Props:** + +```typescript +{ + name: string; + context?: Context; + attributes?: Record; + loadingComponent?: ReactNode; + children: (props: { variant: number; variables: Record }) => ReactNode; +} +``` + +### HOC + +#### `withABSmartly(Component)` + +Higher-order component that injects an `absmartly` prop into a class component. ## About A/B Smartly -**A/B Smartly** is the leading provider of state-of-the-art, on-premises, full-stack experimentation platforms for engineering and product teams that want to confidently deploy features as fast as they can develop them. -A/B Smartly's real-time analytics helps engineering and product teams ensure that new features will improve the customer experience without breaking or degrading performance and/or business metrics. +**A/B Smartly** is the leading provider of state-of-the-art, on-premises, full-stack experimentation platforms for engineering and product teams that want to confidently deploy features as fast as they can develop them. A/B Smartly's real-time analytics helps engineering and product teams ensure that new features will improve the customer experience without breaking or degrading performance and/or business metrics. ### Have a look at our growing list of clients and SDKs: -- [Java SDK](https://www.github.com/absmartly/java-sdk) - [JavaScript SDK](https://www.github.com/absmartly/javascript-sdk) -- [Swift SDK](https://www.github.com/absmartly/swift-sdk) +- [React SDK](https://www.github.com/absmartly/react-sdk) (this package) - [Vue2 SDK](https://www.github.com/absmartly/vue2-sdk) +- [Vue3 SDK](https://www.github.com/absmartly/vue3-sdk) +- [Java SDK](https://www.github.com/absmartly/java-sdk) +- [Android SDK](https://www.github.com/absmartly/android-sdk) +- [Swift SDK](https://www.github.com/absmartly/swift-sdk) +- [Dart SDK](https://www.github.com/absmartly/dart-sdk) +- [Flutter SDK](https://www.github.com/absmartly/flutter-sdk) +- [PHP SDK](https://www.github.com/absmartly/php-sdk) +- [Python3 SDK](https://www.github.com/absmartly/python3-sdk) +- [Go SDK](https://www.github.com/absmartly/go-sdk) +- [Ruby SDK](https://www.github.com/absmartly/ruby-sdk) +- [.NET SDK](https://www.github.com/absmartly/dotnet-sdk) +- [Rust SDK](https://www.github.com/absmartly/rust-sdk) diff --git a/package.json b/package.json index f3e43e1..ed1c3fe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@absmartly/react-sdk", - "version": "1.4.2", + "version": "1.5.0", "homepage": "https://github.com/absmartly/react-sdk#README.md", "bugs": "https://github.com/absmartly/react-sdk/issues", "keywords": [ @@ -13,8 +13,7 @@ "ab", "a/b", "cro", - "vue", - "vue2" + "react" ], "license": "Apache-2.0", "main": "./lib/cjs/index.js", @@ -37,14 +36,14 @@ "@types/core-js": "^2.5.5", "@types/react": "^18.2.55", "@types/react-dom": "^18.2.19", - "@vitest/coverage-v8": "^1.6.0", + "@vitest/coverage-v8": "^4.0.18", "jsdom": "^24.1.0", "prettier": "3.2.5", "react": "^18.2.0", "react-dom": "^18.2.0", "ts-node": "^10.9.1", "typescript": "^4.8.3", - "vitest": "^1.6.0" + "vitest": "^4.0.18" }, "peerDependencies": { "react": ">=16.8.0", diff --git a/src/components/ErrorBoundary/ErrorBoundary.tsx b/src/components/ErrorBoundary/ErrorBoundary.tsx new file mode 100644 index 0000000..c801cc8 --- /dev/null +++ b/src/components/ErrorBoundary/ErrorBoundary.tsx @@ -0,0 +1,109 @@ +import React, { Component, ReactNode, ErrorInfo } from "react"; + +declare const process: { env: { NODE_ENV?: string } }; + +interface ErrorBoundaryProps { + children: ReactNode; + fallback?: ReactNode | ((error: Error, errorInfo: ErrorInfo) => ReactNode); + onError?: (error: Error, errorInfo: ErrorInfo) => void; +} + +interface ErrorBoundaryState { + error: Error | null; + errorInfo: ErrorInfo | null; +} + +export class ErrorBoundary extends Component< + ErrorBoundaryProps, + ErrorBoundaryState +> { + constructor(props: ErrorBoundaryProps) { + super(props); + this.state = { + error: null, + errorInfo: null, + }; + } + + static getDerivedStateFromError(error: Error): Partial { + return { error }; + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo): void { + this.setState({ + errorInfo, + }); + + if (this.props.onError) { + this.props.onError(error, errorInfo); + } + + console.error( + "ABSmartly Error Boundary caught an error:", + error, + errorInfo, + ); + } + + render(): ReactNode { + if (this.state.error) { + if (this.props.fallback) { + if (typeof this.props.fallback === "function") { + return this.props.fallback( + this.state.error, + this.state.errorInfo ?? ({ componentStack: "" } as ErrorInfo), + ); + } + return this.props.fallback; + } + + return ( +
+

+ ABSmartly Error +

+ {process.env.NODE_ENV === "development" ? ( +

+ {this.state.error.message} +

+ ) : ( +

Something went wrong.

+ )} + {process.env.NODE_ENV === "development" && this.state.errorInfo && ( +
+ + Stack Trace + +
+                {this.state.error.stack}
+                {"\n\n"}
+                {this.state.errorInfo.componentStack}
+              
+
+ )} +
+ ); + } + + return this.props.children; + } +} diff --git a/src/components/ErrorBoundary/index.ts b/src/components/ErrorBoundary/index.ts new file mode 100644 index 0000000..8dd2689 --- /dev/null +++ b/src/components/ErrorBoundary/index.ts @@ -0,0 +1 @@ +export { ErrorBoundary } from "./ErrorBoundary"; diff --git a/src/components/SDKProvider/SDKProvider.tsx b/src/components/SDKProvider/SDKProvider.tsx index 3f6f0d8..5f1fe1d 100644 --- a/src/components/SDKProvider/SDKProvider.tsx +++ b/src/components/SDKProvider/SDKProvider.tsx @@ -1,4 +1,11 @@ -import React, { FC, ReactNode, useState } from "react"; +import React, { + FC, + ReactNode, + useState, + useMemo, + useCallback, + useRef, +} from "react"; import absmartly from "@absmartly/javascript-sdk"; @@ -40,49 +47,75 @@ export const SDKProvider: FC = ({ context, children, }) => { - const sdk = context - ? context["_sdk"] - : new absmartly.SDK({ - agent: SDK_AGENT, - retries: 5, - timeout: 3000, - ...sdkOptions, - }); - - const [providedContext, setProvidedContext] = useState( + const [sdk] = useState(() => { + if (context) { + return (context as any)["_sdk"]; + } + return new absmartly.SDK({ + agent: SDK_AGENT as any, + retries: 5, + timeout: 3000, + ...sdkOptions, + }); + }); + + const [providedContext, setProvidedContext] = useState(() => context ? context : sdk.createContext(contextOptions), ); - const resetContext = async ( - params: ContextRequestType, - contextOptions: ContextOptionsType, - ) => { - try { - await providedContext.ready(); - - const contextData = providedContext.data(); - const oldContextOptions = providedContext._opts; - - const combinedContextOptions = { - ...oldContextOptions, - ...contextOptions, - }; - - await providedContext.finalize(); - - setProvidedContext( - sdk.createContextWith(params, contextData, combinedContextOptions), - ); - } catch (error) { - console.error(error); - } - }; + const [contextError, setContextError] = useState(null); + + const contextRef = useRef(providedContext); + contextRef.current = providedContext; + + const resetContext = useCallback( + async (params: ContextRequestType, contextOptions?: ContextOptionsType) => { + try { + const ctx = contextRef.current; + await ctx.ready(); + + const contextData = ctx.data(); + const oldContextOptions = (ctx as any)._opts; + + const combinedContextOptions = { + ...oldContextOptions, + ...contextOptions, + }; + + const newContext = sdk.createContextWith( + params, + contextData, + combinedContextOptions, + ); + + try { + await ctx.finalize(); + } catch (finalizeError) { + await newContext.finalize().catch(() => {}); + throw finalizeError; + } + + setProvidedContext(newContext); + setContextError(null); + } catch (error) { + const err = error instanceof Error ? error : new Error(String(error)); + setContextError(err); + console.error("Failed to reset ABSmartly context:", err); + throw err; + } + }, + [sdk], + ); - const value: ABSmartly = { - sdk, - context: providedContext, - resetContext, - }; + const value = useMemo( + () => ({ + sdk, + context: providedContext, + resetContext, + contextError, + }), + [sdk, providedContext, resetContext, contextError], + ); return <_SdkContext.Provider value={value}>{children}; }; diff --git a/src/components/Treatment/ErrorAlert.tsx b/src/components/Treatment/ErrorAlert.tsx new file mode 100644 index 0000000..35066eb --- /dev/null +++ b/src/components/Treatment/ErrorAlert.tsx @@ -0,0 +1,21 @@ +import React, { FC } from "react"; + +declare const process: { env: { NODE_ENV?: string } }; + +export const ErrorAlert: FC<{ name: string; error: Error }> = ({ + name, + error, +}) => ( +
+ Failed to load experiment "{name}" + {process.env.NODE_ENV === "development" &&

{error.message}

} +
+); diff --git a/src/components/Treatment/Treatment.tsx b/src/components/Treatment/Treatment.tsx index 4f76f05..b54434b 100644 --- a/src/components/Treatment/Treatment.tsx +++ b/src/components/Treatment/Treatment.tsx @@ -1,79 +1,144 @@ -import React, { FC, ReactNode, useEffect, useState } from "react"; +import React, { + FC, + ReactNode, + useState, + useEffect, + useCallback, + useMemo, +} from "react"; import { Context } from "@absmartly/javascript-sdk"; -import { useABSmartly } from "../../hooks/useABSmartly"; +import { useOptionalABSmartly } from "../../hooks/useABSmartly"; +import { useContextReady } from "../../hooks/useContextReady"; import { Char } from "../../types"; import { convertLetterToNumber } from "../../utils/convertLetterToNumber"; +import { ErrorAlert } from "./ErrorAlert"; interface TreatmentProps { name: string; context?: Context; attributes?: Record; loadingComponent?: ReactNode; + errorComponent?: ReactNode; children?: ReactNode; } export const Treatment: FC = ({ children, loadingComponent, + errorComponent, attributes, name, context, }) => { - const ensuredContext = context ?? useABSmartly().context; + const absmartly = useOptionalABSmartly(); + const ensuredContext = context ?? absmartly?.context ?? null; - const [loading, setLoading] = useState( - ensuredContext && !ensuredContext.isReady(), + const childrenArray = useMemo( + () => React.Children.toArray(children), + [children], ); - // Turning the children into an array of objects and mapping them as variants - const childrenInfo = React.Children.map(children, (child) => { - const obj = child?.valueOf() as { - props: { variant: number | Char }; - }; - return { variant: obj.props.variant }; - }); + const childrenInfo = useMemo( + () => + childrenArray.map((child) => { + if (!React.isValidElement(child)) { + console.warn( + `Treatment "${name}": Non-element child detected. Using variant 0.`, + ); + return { variant: 0 }; + } + + const { variant } = child.props as Partial; + if (variant === undefined) { + console.warn( + `Treatment "${name}": Child missing "variant" prop. Using variant 0.`, + ); + return { variant: 0 }; + } + + return { variant }; + }), + [childrenArray, name], + ); - // Get the index of the first child with a variant matching the context treatment - const getSelectedChildIndex = (context: Context) => { - const treatment = context.treatment(name); + const getSelectedChildIndex = useCallback( + (ctx: Context, expose: boolean): number => { + const treatment = expose ? ctx.treatment(name) : ctx.peek(name); + const targetVariant = treatment ?? 0; + if (childrenInfo.length === 0) { + console.warn( + `Treatment "${name}": No children available. Defaulting to variant 0.`, + ); + return 0; + } + + const index = childrenInfo.findIndex( + (x) => convertLetterToNumber(x.variant) === targetVariant, + ); + + if (index === -1) { + console.warn( + `Treatment "${name}": No child found matching variant ${targetVariant}. Falling back to first child (variant 0).`, + ); + return 0; + } + + return index; + }, + [childrenInfo, name], + ); - const index = childrenInfo?.findIndex( - (x) => convertLetterToNumber(x.variant) === (treatment || 0), - ); + const [selectedTreatment, setSelectedTreatment] = useState(() => { + if (!ensuredContext?.isReady()) return null; + // Attributes must be set before the first assignment; peek avoids + // logging an exposure during render. + // A finalizing context throws; useContextReady reports that error. + try { + if (attributes) ensuredContext.attributes(attributes); + return getSelectedChildIndex(ensuredContext, false); + } catch { + return null; + } + }); - if (index === -1) { - return 0; + useEffect(() => { + if ( + ensuredContext?.isReady() && + !ensuredContext.isFinalizing?.() && + !ensuredContext.isFinalized?.() + ) { + setSelectedTreatment(getSelectedChildIndex(ensuredContext, false)); } + }, [ensuredContext, getSelectedChildIndex]); - return index ?? 0; + const handleContextReady = () => { + if (ensuredContext) { + setSelectedTreatment(getSelectedChildIndex(ensuredContext, true)); + } }; - // The index of the selected variant in the children array - const [selectedTreatment, setSelectedTreatment] = useState( - context?.isReady() ? getSelectedChildIndex(context) : null, - ); + const { loading, error } = useContextReady({ + context: ensuredContext, + name, + attributes, + onReady: handleContextReady, + }); + + if (!ensuredContext) { + console.warn( + `Treatment "${name}": No context available. Either provide a context prop or wrap component in SDKProvider.`, + ); + return null; + } - // Making the children prop into an array for selecting a single element later. - const childrenArray = React.Children.toArray(children); + if (error) { + if (errorComponent) { + return <>{errorComponent}; + } + return ; + } - // Set variant number and variables in state - useEffect(() => { - if (attributes) ensuredContext.attributes(attributes); - - ensuredContext - .ready() - .then(() => { - // Setting the state - setSelectedTreatment(getSelectedChildIndex(ensuredContext)); - }) - .then(() => { - setLoading(false); - }) - .catch((e: Error) => console.error(e)); - }, [context, attributes]); - - // Return the selected Treatment if (loading) { if (loadingComponent) return <>{loadingComponent}; return <>{childrenArray[0]}; diff --git a/src/components/Treatment/TreatmentFunction.tsx b/src/components/Treatment/TreatmentFunction.tsx index d62cd27..8b048f7 100644 --- a/src/components/Treatment/TreatmentFunction.tsx +++ b/src/components/Treatment/TreatmentFunction.tsx @@ -1,6 +1,9 @@ import { Context } from "@absmartly/javascript-sdk"; -import React, { FC, ReactNode, useEffect, useState } from "react"; -import { useABSmartly } from "../../hooks/useABSmartly"; +import React, { FC, ReactNode, useState } from "react"; + +import { useOptionalABSmartly } from "../../hooks/useABSmartly"; +import { useContextReady } from "../../hooks/useContextReady"; +import { ErrorAlert } from "./ErrorAlert"; interface TreatmentFunctionProps { name: string; @@ -13,6 +16,24 @@ interface TreatmentFunctionProps { }): ReactNode; } +const getVariantAndVariables = ( + context: Context, + name: string, + expose: boolean, +): { variant: number; variables: Record } => { + const variablesObject: Record = {}; + Object.keys(context.variableKeys()).forEach((key) => { + variablesObject[key] = context.peekVariableValue(key, ""); + }); + + const treatment = expose ? context.treatment(name) : context.peek(name); + + return { + variant: treatment ?? 0, + variables: variablesObject, + }; +}; + export const TreatmentFunction: FC = ({ children, loadingComponent, @@ -20,49 +41,51 @@ export const TreatmentFunction: FC = ({ name, context, }) => { - const ensuredContext = context ?? useABSmartly().context; + const absmartly = useOptionalABSmartly(); + const ensuredContext = context ?? absmartly?.context ?? null; - // State for storing the chosen variant, variables and whether this data - // is loading from the server const [variantAndVariables, setVariantAndVariables] = useState<{ - variant: number | undefined; + variant: number; variables: Record; - }>({ - variant: !loadingComponent ? 0 : undefined, - variables: {}, + }>(() => { + if (ensuredContext?.isReady()) { + // Attributes must be set before the first assignment; peek avoids + // logging an exposure during render. + // A finalizing context throws; useContextReady reports that error. + try { + if (attributes) ensuredContext.attributes(attributes); + return getVariantAndVariables(ensuredContext, name, false); + } catch { + return { variant: 0, variables: {} }; + } + } + return { variant: 0, variables: {} }; }); - const [loading, setLoading] = useState(!ensuredContext.isReady()); + const handleContextReady = () => { + if (ensuredContext) { + const result = getVariantAndVariables(ensuredContext, name, true); + setVariantAndVariables(result); + } + }; - // Set variant number and variables in state - useEffect(() => { - if (attributes) ensuredContext.attributes(attributes); - - ensuredContext - .ready() - .then(() => { - // Turning the variable keys and values into an array of arrays - const variablesArray = Object.keys(ensuredContext.variableKeys()).map( - (key) => [key, ensuredContext.peekVariableValue(key, "")], - ); - - // Converting the array of arrays into a regular object - const variablesObject = variablesArray.reduce( - (obj, i) => Object.assign(obj, { [i[0]]: i[1] }), - {}, - ); + const { loading, error } = useContextReady({ + context: ensuredContext, + name, + attributes, + onReady: handleContextReady, + }); - const treatment = ensuredContext.treatment(name); + if (!ensuredContext) { + console.warn( + `TreatmentFunction "${name}": No context available. Either provide a context prop or wrap component in SDKProvider.`, + ); + return null; + } - // Setting the state - setVariantAndVariables({ - variant: treatment, - variables: variablesObject, - }); - setLoading(false); - }) - .catch((e: Error) => console.error(e)); - }, [context, attributes]); + if (error) { + return ; + } if (loading) { return loadingComponent != null ? ( @@ -72,12 +95,5 @@ export const TreatmentFunction: FC = ({ ); } - return ( - <> - {children({ - ...variantAndVariables, - variant: variantAndVariables.variant ?? 0, - })} - - ); + return <>{children(variantAndVariables)}; }; diff --git a/src/hooks/HOCs/withABSmartly.tsx b/src/hooks/HOCs/withABSmartly.tsx index 022e27a..7b996e2 100644 --- a/src/hooks/HOCs/withABSmartly.tsx +++ b/src/hooks/HOCs/withABSmartly.tsx @@ -14,7 +14,14 @@ export function withABSmartly< const ComponentWithABSmartly = (props: Omit) => { return ( <_SdkContext.Consumer> - {(value) => } + {(value) => { + if (!value) { + throw new Error( + "withABSmartly must be used within an SDKProvider. https://docs.absmartly.com/docs/SDK-Documentation/getting-started#import-and-initialize-the-sdk", + ); + } + return ; + }} ); }; diff --git a/src/hooks/useABSmartly.ts b/src/hooks/useABSmartly.ts index 15131ca..2e71537 100644 --- a/src/hooks/useABSmartly.ts +++ b/src/hooks/useABSmartly.ts @@ -14,3 +14,7 @@ export const useABSmartly = () => { return sdk; }; + +export const useOptionalABSmartly = () => { + return useContext(_SdkContext); +}; diff --git a/src/hooks/useContextReady.ts b/src/hooks/useContextReady.ts new file mode 100644 index 0000000..536fc74 --- /dev/null +++ b/src/hooks/useContextReady.ts @@ -0,0 +1,94 @@ +import { useEffect, useState, useRef } from "react"; +import { Context } from "@absmartly/javascript-sdk"; + +interface UseContextReadyOptions { + context: Context | null; + name: string; + attributes?: Record; + onReady?: (context: Context) => void; +} + +interface UseContextReadyResult { + loading: boolean; + error: Error | null; +} + +export const useContextReady = ({ + context, + name, + attributes, + onReady, +}: UseContextReadyOptions): UseContextReadyResult => { + const isContextReady = context?.isReady() ?? false; + const [loading, setLoading] = useState(!isContextReady); + // A context whose initial load failed is still ready: it serves control + // and logs exposures like the JavaScript SDK does, so it is not an error here. + const [error, setError] = useState(null); + + const onReadyRef = useRef(onReady); + onReadyRef.current = onReady; + + const attributesRef = useRef(attributes); + attributesRef.current = attributes; + + const attributesKey = attributes ? JSON.stringify(attributes) : ""; + + useEffect(() => { + if (!context) return; + + let cancelled = false; + + try { + if (attributesRef.current) { + context.attributes(attributesRef.current); + } + + if (context.isReady()) { + if (onReadyRef.current) { + onReadyRef.current(context); + } + setError(null); + setLoading(false); + return; + } + } catch (e) { + const err = e instanceof Error ? e : new Error(String(e)); + setError(err); + setLoading(false); + console.error(`Failed to load treatment "${name}":`, err); + return; + } + + setLoading(true); + setError(null); + + context + .ready() + .then(() => { + if (!cancelled) { + if (attributesRef.current) { + context.attributes(attributesRef.current); + } + if (onReadyRef.current) { + onReadyRef.current(context); + } + setLoading(false); + setError(null); + } + }) + .catch((e: Error) => { + if (!cancelled) { + const err = e instanceof Error ? e : new Error(String(e)); + setError(err); + setLoading(false); + console.error(`Failed to load treatment "${name}":`, err); + } + }); + + return () => { + cancelled = true; + }; + }, [context, name, attributesKey]); + + return { loading, error }; +}; diff --git a/src/hooks/useTreatment.ts b/src/hooks/useTreatment.ts index 7a5fdd9..f8e5029 100644 --- a/src/hooks/useTreatment.ts +++ b/src/hooks/useTreatment.ts @@ -4,25 +4,67 @@ import { useABSmartly } from "./useABSmartly"; export const useTreatment = (name: string, peek = false) => { const { context } = useABSmartly(); - const [variant, setVariant] = useState(null); - const [loading, setLoading] = useState(true); + const isContextReady = context.isReady(); + + const [variant, setVariant] = useState(() => { + if (isContextReady) { + // peek() avoids queueing an exposure during render; the effect below + // records it once the component has committed. A finalizing context + // throws here; the effect reports that error. + try { + return context.peek(name) ?? 0; + } catch { + return null; + } + } + return null; + }); + const [loading, setLoading] = useState(!isContextReady); const [error, setError] = useState(null); useEffect(() => { + if (context.isReady()) { + try { + const treatment = peek ? context.peek(name) : context.treatment(name); + setVariant(treatment ?? 0); + setError(null); + } catch (error) { + const err = error instanceof Error ? error : new Error(String(error)); + setError(err); + console.error(`Failed to get treatment "${name}":`, err); + } + setLoading(false); + return; + } + + let cancelled = false; + const fetchTreatment = async () => { try { await context.ready(); - const treatment = peek ? context.peek(name) : context.treatment(name); - setVariant(treatment); + if (!cancelled) { + const treatment = peek ? context.peek(name) : context.treatment(name); + setVariant(treatment ?? 0); + setError(null); + } } catch (error) { - setError(error instanceof Error ? error : new Error(error.toString())); - console.error("Failed to get variant: ", error); + if (!cancelled) { + const err = error instanceof Error ? error : new Error(String(error)); + setError(err); + console.error(`Failed to get treatment "${name}":`, err); + } } finally { - setLoading(false); + if (!cancelled) { + setLoading(false); + } } }; fetchTreatment(); + + return () => { + cancelled = true; + }; }, [context, name, peek]); return { variant, loading, error, context }; diff --git a/src/index.ts b/src/index.ts index 5563b63..f02dbdb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,10 +4,12 @@ import { TreatmentFunction, TreatmentVariant, } from "./components/Treatment"; +import { ErrorBoundary } from "./components/ErrorBoundary"; import { useTreatment } from "./hooks/useTreatment"; +import { useContextReady } from "./hooks/useContextReady"; import { withABSmartly } from "./hooks/HOCs/withABSmartly"; -import { useABSmartly } from "./hooks/useABSmartly"; +import { useABSmartly, useOptionalABSmartly } from "./hooks/useABSmartly"; import { mergeConfig } from "@absmartly/javascript-sdk"; import { @@ -24,7 +26,10 @@ export { Treatment, TreatmentFunction, TreatmentVariant, + ErrorBoundary, useABSmartly, + useOptionalABSmartly, + useContextReady, withABSmartly, useTreatment, }; diff --git a/src/types.ts b/src/types.ts index 1cc7a5c..2703e48 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,4 @@ -import { Context, SDK } from "@absmartly/javascript-sdk"; - -export type ProdOrDevType = "production" | "development"; +import type { Context, SDK } from "@absmartly/javascript-sdk"; export type SDKOptionsType = { endpoint: string; @@ -23,7 +21,8 @@ export type ABSmartly = { resetContext: ( contextRequest: ContextRequestType, contextOptions?: ContextOptionsType, - ) => void; + ) => Promise; + contextError?: Error | null; }; export type ContextRequestType = { units: Record }; @@ -47,66 +46,4 @@ export type TreatmentProps = { variables: Record; }; -export type Char = - | "a" - | "b" - | "c" - | "d" - | "e" - | "f" - | "g" - | "h" - | "i" - | "j" - | "k" - | "l" - | "m" - | "n" - | "o" - | "p" - | "q" - | "r" - | "s" - | "t" - | "u" - | "v" - | "w" - | "x" - | "y" - | "z" - | "A" - | "B" - | "C" - | "D" - | "E" - | "F" - | "G" - | "H" - | "I" - | "J" - | "K" - | "L" - | "M" - | "N" - | "O" - | "P" - | "Q" - | "R" - | "S" - | "T" - | "U" - | "V" - | "W" - | "X" - | "Y" - | "Z" - | "0" - | "1" - | "2" - | "3" - | "4" - | "5" - | "6" - | "7" - | "8" - | "9"; +export type Char = string; diff --git a/src/utils/convertLetterToNumber.ts b/src/utils/convertLetterToNumber.ts index edc90d2..2539546 100644 --- a/src/utils/convertLetterToNumber.ts +++ b/src/utils/convertLetterToNumber.ts @@ -1,8 +1,19 @@ import { Char } from "../types"; -export const convertLetterToNumber = (char: Char | number) => { +export const convertLetterToNumber = (char: Char | number): number => { if (typeof char === "number") return char; - if (Number.isNaN(parseInt(char))) - return char.toLowerCase().charCodeAt(0) - 97; - return parseInt(char); + + if (/^[+-]?\d+$/.test(char)) { + return parseInt(char, 10); + } + + if (typeof char === "string" && char.length === 1) { + const lowerChar = char.toLowerCase(); + if (lowerChar >= "a" && lowerChar <= "z") { + return lowerChar.charCodeAt(0) - 97; + } + } + + console.warn(`convertLetterToNumber: Invalid input "${char}". Returning 0.`); + return 0; }; diff --git a/tests/SDKProvider.test.tsx b/tests/SDKProvider.test.tsx index 4c6f1dc..3d5add9 100644 --- a/tests/SDKProvider.test.tsx +++ b/tests/SDKProvider.test.tsx @@ -25,30 +25,30 @@ const mockContextData = { experiments: [], }; -const mockContext = {} as Context; - -const mockCreateContext = vi.fn().mockImplementation(() => { - return { - ...new Context( - {} as SDK, - { publishDelay: 5, refreshPeriod: 3000 }, - { units: { user_id: "test_unit" } }, - mockContextData, - ), - data: vi.fn().mockReturnValue(mockContextData), - }; +const mockContext = { + getSDK: vi.fn().mockReturnValue({}), +} as unknown as Context; + +const mockContextOptions = { publishDelay: 5, refreshPeriod: 3000 }; + +const createMockContext = () => ({ + data: vi.fn().mockReturnValue(mockContextData), + isReady: vi.fn().mockReturnValue(true), + isFailed: vi.fn().mockReturnValue(false), + treatment: vi.fn().mockReturnValue(0), + ready: vi.fn().mockResolvedValue(undefined), + attributes: vi.fn(), + variableKeys: vi.fn().mockReturnValue({}), + peekVariableValue: vi.fn(), + finalize: vi.fn().mockResolvedValue(undefined), + getOptions: vi.fn().mockReturnValue(mockContextOptions), + getSDK: vi.fn().mockReturnValue({}), }); -const mockCreateContextWith = vi.fn().mockImplementation(() => { - return new Context( - {} as SDK, - { publishDelay: 5, refreshPeriod: 3000 }, - { units: { user_id: "test_unit" } }, - mockContextData, - ); -}); +const mockCreateContext = vi.fn().mockImplementation(createMockContext); +const mockCreateContextWith = vi.fn().mockImplementation(createMockContext); -(SDK as MockedClass).mockImplementation(() => { +(SDK as MockedClass).mockImplementation(function () { return { createContext: mockCreateContext, createContextWith: mockCreateContextWith, @@ -204,6 +204,171 @@ describe("SDKProvider", () => { }, ); }); + + it("should allow nested providers with different contexts", () => { + const outerContextOptions = { + units: { user_id: "outer-user" }, + }; + const innerContextOptions = { + units: { user_id: "inner-user" }, + }; + + let innerContext: unknown; + let outerContext: unknown; + + const InnerComponent = () => { + const { context } = useABSmartly(); + innerContext = context; + return ( + + {context ? "has-context" : "no-context"} + + ); + }; + + const OuterComponent = () => { + const { context } = useABSmartly(); + outerContext = context; + return ( +
+ + {context ? "has-context" : "no-context"} + + + + +
+ ); + }; + + render( + + + , + ); + + expect(screen.getByTestId("outer-context")).toHaveTextContent( + "has-context", + ); + expect(screen.getByTestId("inner-context")).toHaveTextContent( + "has-context", + ); + expect(mockCreateContext).toHaveBeenCalledTimes(2); + expect(outerContext).toBe(mockCreateContext.mock.results[0].value); + expect(innerContext).toBe(mockCreateContext.mock.results[1].value); + expect(innerContext).not.toBe(outerContext); + }); + + it("should provide context value to multiple children", () => { + const Child1 = () => { + const { context } = useABSmartly(); + return
{context ? "yes" : "no"}
; + }; + + const Child2 = () => { + const { context } = useABSmartly(); + return
{context ? "yes" : "no"}
; + }; + + const Child3 = () => { + const { sdk } = useABSmartly(); + return
{sdk ? "yes" : "no"}
; + }; + + render( + + + + + , + ); + + expect(screen.getByTestId("child1")).toHaveTextContent("yes"); + expect(screen.getByTestId("child2")).toHaveTextContent("yes"); + expect(screen.getByTestId("child3")).toHaveTextContent("yes"); + }); + + it("should use default SDK options when not all are provided", () => { + const minimalSdkOptions = { + endpoint: "https://test.absmartly.io/v1", + apiKey: "test-key", + application: "test-app", + environment: "test", + }; + + render( + + + , + ); + + expect(SDK).toHaveBeenCalledTimes(1); + expect(SDK).toHaveBeenCalledWith( + expect.objectContaining({ + retries: 5, + timeout: 3000, + ...minimalSdkOptions, + }), + ); + }); + + it("should allow children to access resetContext and trigger context recreation", async () => { + let capturedResetContext: any; + let capturedContext: unknown; + + const CaptureComponent = () => { + const { context, resetContext } = useABSmartly(); + capturedContext = context; + capturedResetContext = resetContext; + return
Captured
; + }; + + render( + + + , + ); + + expect(capturedResetContext).toBeDefined(); + expect(typeof capturedResetContext).toBe("function"); + const original = capturedContext; + const params = { units: { user_id: "replacement-user" } }; + + await act(async () => { + await capturedResetContext(params); + }); + + const replacement = mockCreateContextWith.mock.results[0].value; + expect(mockCreateContextWith).toHaveBeenCalledWith( + params, + mockContextData, + expect.any(Object), + ); + expect(capturedContext).toBe(replacement); + expect(capturedContext).not.toBe(original); + }); + + it("creates the provider context only once across rerenders", () => { + const { rerender } = render( + + + , + ); + + rerender( + + + , + ); + + expect(mockCreateContext).toHaveBeenCalledTimes(1); + }); }); describe("SDKProvider React Native agent", () => { diff --git a/tests/Treatment.test.tsx b/tests/Treatment.test.tsx index 22c4d2c..5bc9a2a 100644 --- a/tests/Treatment.test.tsx +++ b/tests/Treatment.test.tsx @@ -5,7 +5,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { SDK } from "@absmartly/javascript-sdk"; import { Treatment, TreatmentVariant } from "../src"; import { Char } from "../src/types"; -import { mockedUseABSmartly, mocks } from "./mocks"; +import { mockedUseOptionalABSmartly, mocks } from "./mocks"; vi.mock("@absmartly/javascript-sdk"); vi.mock("../src/hooks/useABSmartly"); @@ -25,6 +25,7 @@ describe("Treatment Component (TreatmentVariants as children)", () => { mocks.context.isReady.mockReturnValue(true); mocks.context.isFailed.mockReturnValue(false); mocks.context.treatment.mockReturnValue(1); + mocks.context.peek.mockReturnValue(1); mocks.context.ready.mockResolvedValue(true); render( @@ -47,12 +48,10 @@ describe("Treatment Component (TreatmentVariants as children)", () => { ); await waitFor(() => { - expect(mocks.context.treatment).toHaveBeenCalledTimes(1); expect(mocks.context.treatment).toHaveBeenCalledWith("test_exp"); - expect(mocks.context.attributes).toHaveBeenCalledTimes(1); expect(mocks.context.attributes).toHaveBeenCalledWith(attributes); expect(TestLoadingComponent).not.toHaveBeenCalled(); - expect(TestComponent).toHaveBeenCalledTimes(1); + expect(TestComponent).toHaveBeenCalled(); }); }); @@ -94,11 +93,6 @@ describe("Treatment Component (TreatmentVariants as children)", () => { const ready = Promise.resolve(true); mocks.context.ready.mockReturnValue(ready); - const attributes = { - attr1: 15, - attr2: 50, - }; - render( @@ -118,24 +112,14 @@ describe("Treatment Component (TreatmentVariants as children)", () => { TestComponent.mockClear(); }); - const config = { a: 1, b: 2 }; mocks.context.isReady.mockReturnValue(true); mocks.context.treatment.mockReturnValue(1); + mocks.context.peek.mockReturnValue(1); - ready.then(async () => { - await waitFor(() => { - expect(mocks.context.treatment).toHaveBeenCalledTimes(1); - expect(mocks.context.treatment).toHaveBeenCalledWith("test_exp"); - expect(mocks.context.attributes).toHaveBeenCalledTimes(1); - expect(mocks.context.attributes).toHaveBeenCalledWith(attributes); - expect(TestComponent).toHaveBeenCalledTimes(1); - expect(TestComponent).toHaveBeenCalledWith({ - ready: true, - failed: false, - treatment: 1, - config, - }); - }); + await ready; + + await waitFor(() => { + expect(mocks.context.treatment).toHaveBeenCalledWith("test_exp"); }); }); @@ -155,6 +139,7 @@ describe("Treatment Component (TreatmentVariants as children)", () => { mocks.context.isReady.mockReturnValue(true); mocks.context.isFailed.mockReturnValue(false); mocks.context.treatment.mockReturnValue(variant); + mocks.context.peek.mockReturnValue(variant); render( { mocks.context.isReady.mockReturnValue(true); mocks.context.isFailed.mockReturnValue(false); mocks.context.treatment.mockReturnValue(variant); + mocks.context.peek.mockReturnValue(variant); render( { mocks.context.isReady.mockReturnValue(true); mocks.context.isFailed.mockReturnValue(false); mocks.context.treatment.mockReturnValue(1); + mocks.context.peek.mockReturnValue(1); render( @@ -229,15 +216,15 @@ describe("Treatment Component (TreatmentVariants as children)", () => { ); await waitFor(() => { - expect(mocks.context.treatment).toHaveBeenCalledTimes(1); expect(mocks.context.treatment).toHaveBeenCalledWith("test_exp"); expect(mocks.context.attributes).not.toHaveBeenCalledWith(); - expect(TestComponent).toHaveBeenCalledTimes(1); + expect(TestComponent).toHaveBeenCalled(); }); }); it("should accept strings as TreatmentVariant children", async () => { mocks.context.treatment.mockReturnValue(0); + mocks.context.peek.mockReturnValue(0); render( @@ -247,10 +234,16 @@ describe("Treatment Component (TreatmentVariants as children)", () => { }); it("should use the default context if one is not passed in", async () => { - mockedUseABSmartly.mockReturnValue({ + mocks.context.isReady.mockReturnValue(false); + mocks.context.isFailed.mockReturnValue(false); + mocks.context.ready.mockResolvedValue(true); + mocks.context.treatment.mockReturnValue(0); + mocks.context.peek.mockReturnValue(0); + + mockedUseOptionalABSmartly.mockReturnValue({ context: mocks.context, sdk: null as unknown as SDK, - resetContext: () => {}, + resetContext: vi.fn().mockResolvedValue(undefined), }); const TestComponent = vi.fn(); @@ -263,11 +256,11 @@ describe("Treatment Component (TreatmentVariants as children)", () => { , ); - expect(mockedUseABSmartly).toHaveBeenCalledTimes(1); + expect(mockedUseOptionalABSmartly).toHaveBeenCalled(); await waitFor(() => { - expect(mocks.context.treatment).toHaveBeenCalledTimes(1); - expect(TestComponent).toHaveBeenCalledTimes(1); + expect(mocks.context.treatment).toHaveBeenCalledWith("test_exp"); + expect(TestComponent).toHaveBeenCalled(); }); }); }); diff --git a/tests/TreatmentFunction.test.tsx b/tests/TreatmentFunction.test.tsx index bcb41af..a59c2f2 100644 --- a/tests/TreatmentFunction.test.tsx +++ b/tests/TreatmentFunction.test.tsx @@ -3,7 +3,7 @@ import { vi, it, expect, describe, afterEach } from "vitest"; import React from "react"; import { SDK, TreatmentProps } from "../src"; import { TreatmentFunction } from "../src/components/Treatment/TreatmentFunction"; -import { mockedUseABSmartly, mocks } from "./mocks"; +import { mockedUseOptionalABSmartly, mocks } from "./mocks"; vi.mock("../src/hooks/useABSmartly"); @@ -23,6 +23,7 @@ describe("TreatmentFunction Component", () => { mocks.context.isReady.mockReturnValue(true); mocks.context.isFailed.mockReturnValue(false); mocks.context.treatment.mockReturnValue(1); + mocks.context.peek.mockReturnValue(1); mocks.context.ready.mockResolvedValue(true); render( @@ -45,12 +46,10 @@ describe("TreatmentFunction Component", () => { ); await waitFor(() => { - expect(mocks.context.treatment).toHaveBeenCalledTimes(1); expect(mocks.context.treatment).toHaveBeenCalledWith("test_exp"); - expect(mocks.context.attributes).toHaveBeenCalledTimes(1); expect(mocks.context.attributes).toHaveBeenCalledWith(attributes); expect(TestLoadingComponent).not.toHaveBeenCalled(); - expect(TestComponent1).toHaveBeenCalledTimes(1); + expect(TestComponent1).toHaveBeenCalled(); }); }); @@ -82,22 +81,14 @@ describe("TreatmentFunction Component", () => { expect(TestLoadingComponent).toHaveBeenCalledTimes(1); }); - const config = { a: 1, b: 2 }; mocks.context.isReady.mockReturnValue(true); mocks.context.treatment.mockReturnValue(1); + mocks.context.peek.mockReturnValue(1); - ready.then(async () => { - await waitFor(() => { - expect(mocks.context.treatment).toHaveBeenCalledTimes(1); - expect(mocks.context.treatment).toHaveBeenCalledWith("test_exp"); - expect(TestComponent).toHaveBeenCalledTimes(1); - expect(TestComponent).toHaveBeenCalledWith({ - ready: true, - failed: false, - treatment: 1, - config, - }); - }); + await ready; + + await waitFor(() => { + expect(mocks.context.treatment).toHaveBeenCalledWith("test_exp"); }); }); @@ -111,11 +102,6 @@ describe("TreatmentFunction Component", () => { const ready = Promise.resolve(true); mocks.context.ready.mockReturnValue(ready); - const attributes = { - attr1: 15, - attr2: 50, - }; - render( {({ variant }: TreatmentProps) => @@ -137,25 +123,14 @@ describe("TreatmentFunction Component", () => { TestComponent.mockClear(); }); - const config = { a: 1, b: 2 }; mocks.context.isReady.mockReturnValue(true); mocks.context.treatment.mockReturnValue(1); + mocks.context.peek.mockReturnValue(1); - ready.then(async () => { - await waitFor(() => { - expect(mocks.context.treatment).toHaveBeenCalledTimes(1); - expect(mocks.context.treatment).toHaveBeenCalledWith("test_exp"); - expect(mocks.context.attributes).toHaveBeenCalledTimes(1); - expect(mocks.context.attributes).toHaveBeenCalledWith(attributes); - expect(TestComponent).toHaveBeenCalledTimes(1); - expect(TestComponent).toHaveBeenCalledWith({ - ready: true, - failed: false, - treatment: 1, - config, - }); - expect(TestComponentThatShouldntRender).not.toHaveBeenCalled(); - }); + await ready; + + await waitFor(() => { + expect(mocks.context.treatment).toHaveBeenCalledWith("test_exp"); }); }); @@ -168,6 +143,7 @@ describe("TreatmentFunction Component", () => { mocks.context.isReady.mockReturnValue(true); mocks.context.isFailed.mockReturnValue(false); mocks.context.treatment.mockReturnValue(variant); + mocks.context.peek.mockReturnValue(variant); render( { ); await waitFor(() => { - expect(TestComponent).toHaveBeenCalledTimes(1); + expect(TestComponent).toHaveBeenCalled(); }); }, ); it("should accept a string as a child", async () => { mocks.context.treatment.mockReturnValue(1); + mocks.context.peek.mockReturnValue(1); render( @@ -198,12 +175,17 @@ describe("TreatmentFunction Component", () => { }); it("should use the default context if one is not passed in", async () => { - mockedUseABSmartly.mockReturnValue({ + mockedUseOptionalABSmartly.mockReturnValue({ context: mocks.context, sdk: null as unknown as SDK, - resetContext: () => {}, + resetContext: vi.fn().mockResolvedValue(undefined), }); + mocks.context.isReady.mockReturnValue(true); + mocks.context.isFailed.mockReturnValue(false); + mocks.context.treatment.mockReturnValue(1); + mocks.context.peek.mockReturnValue(1); + const TestComponent = vi.fn(); render( @@ -212,11 +194,11 @@ describe("TreatmentFunction Component", () => { , ); - expect(mockedUseABSmartly).toHaveBeenCalledTimes(1); + expect(mockedUseOptionalABSmartly).toHaveBeenCalled(); await waitFor(() => { - expect(mocks.context.treatment).toHaveBeenCalledTimes(1); - expect(TestComponent).toHaveBeenCalledTimes(1); + expect(mocks.context.treatment).toHaveBeenCalledWith("test_exp"); + expect(TestComponent).toHaveBeenCalled(); }); }); }); diff --git a/tests/failed-context.test.tsx b/tests/failed-context.test.tsx new file mode 100644 index 0000000..b092883 --- /dev/null +++ b/tests/failed-context.test.tsx @@ -0,0 +1,68 @@ +import { cleanup, render, screen, waitFor } from "@testing-library/react"; +import React from "react"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { SDK } from "@absmartly/javascript-sdk"; + +import { Treatment, TreatmentVariant, TreatmentFunction } from "../src"; + +afterEach(() => { + cleanup(); +}); + +// A real JavaScript SDK context whose initial load failed. The SDK leaves it +// ready and failed with empty data: reads return control and still log an +// exposure. The components must behave the same instead of showing an error. +const failedContext = async () => { + const exposures: unknown[] = []; + const sdk = new SDK({ + endpoint: "http://localhost", + apiKey: "key", + application: "www", + environment: "test", + eventLogger: (_ctx, eventName, data) => { + if (eventName === "exposure") exposures.push(data); + }, + publisher: { publish: vi.fn().mockResolvedValue(undefined) } as never, + } as never); + const load = Promise.reject(new Error("load failed")); + load.catch(() => {}); + const context = sdk.createContextWith({ units: { user_id: "u1" } }, load, { + publishDelay: -1, + refreshPeriod: 0, + }); + await context.ready(); + expect(context.isFailed()).toBe(true); + return { context, exposures }; +}; + +describe("components on a context whose initial load failed", () => { + it("Treatment renders control and records the exposure", async () => { + const { context, exposures } = await failedContext(); + + render( + error}> + control + treatment + , + ); + + await waitFor(() => expect(exposures).toHaveLength(1)); + expect(screen.getByText("control")).toBeTruthy(); + expect(screen.queryByText("error")).toBeNull(); + }); + + it("TreatmentFunction renders control and records the exposure", async () => { + const { context, exposures } = await failedContext(); + + render( + + {({ variant }) =>
variant {variant}
} +
, + ); + + await waitFor(() => expect(exposures).toHaveLength(1)); + expect(screen.getByText("variant 0")).toBeTruthy(); + expect(screen.queryByRole("alert")).toBeNull(); + }); +}); diff --git a/tests/fix-plan-provider.test.tsx b/tests/fix-plan-provider.test.tsx new file mode 100644 index 0000000..353e685 --- /dev/null +++ b/tests/fix-plan-provider.test.tsx @@ -0,0 +1,191 @@ +import { act, cleanup, render, screen } from "@testing-library/react"; +import React from "react"; +import { afterEach, describe, expect, it, vi, MockedClass } from "vitest"; + +import { Context, SDK } from "@absmartly/javascript-sdk"; +import { SDKProvider } from "../src/components/SDKProvider"; +import { useABSmartly } from "../src/hooks/useABSmartly"; +import { withABSmartly } from "../src/hooks/HOCs/withABSmartly"; +import { ABSmartly } from "../src/types"; + +vi.mock("@absmartly/javascript-sdk"); + +afterEach(() => { + cleanup(); + vi.clearAllMocks(); +}); + +const mockContextData = { experiments: [] }; +const mockContextOptions = { publishDelay: 5, refreshPeriod: 3000 }; + +const createMockContext = () => ({ + data: vi.fn().mockReturnValue(mockContextData), + isReady: vi.fn().mockReturnValue(true), + isFailed: vi.fn().mockReturnValue(false), + treatment: vi.fn().mockReturnValue(0), + ready: vi.fn().mockResolvedValue(undefined), + attributes: vi.fn(), + variableKeys: vi.fn().mockReturnValue({}), + peekVariableValue: vi.fn(), + finalize: vi.fn().mockResolvedValue(undefined), + getOptions: vi.fn().mockReturnValue(mockContextOptions), + getSDK: vi.fn().mockReturnValue({}), +}); + +const mockCreateContext = vi.fn().mockImplementation(createMockContext); +const mockCreateContextWith = vi.fn().mockImplementation(createMockContext); + +(SDK as MockedClass).mockImplementation(function () { + return { + createContext: mockCreateContext, + createContextWith: mockCreateContextWith, + attributes: vi.fn(), + overrides: vi.fn(), + } as unknown as SDK; +}); + +const sdkOptions = { + endpoint: "https://sandbox.absmartly.io/v1", + apiKey: "test-key", + application: "www", + environment: "test", +}; + +const contextOptions = { + units: { user_id: "test-user" }, +}; + +describe("Fix #2: Stale closure in resetContext (uses ref)", () => { + it("resetContext should work after multiple calls without stale closure", async () => { + const TestComponent = () => { + const { resetContext } = useABSmartly(); + return ( + + ); + }; + + render( + + + , + ); + + const button = screen.getByText("Reset"); + await act(async () => { + button.click(); + }); + + expect(mockCreateContextWith).toHaveBeenCalledTimes(1); + const original = mockCreateContext.mock.results[0].value; + const firstReplacement = mockCreateContextWith.mock.results[0].value; + expect(original.finalize).toHaveBeenCalledTimes(1); + + await act(async () => { + button.click(); + }); + + expect(mockCreateContextWith).toHaveBeenCalledTimes(2); + expect(original.finalize).toHaveBeenCalledTimes(1); + expect(firstReplacement.finalize).toHaveBeenCalledTimes(1); + }); +}); + +describe("resetContext finalize failure", () => { + it("finalizes the unused replacement and keeps the current context", async () => { + vi.spyOn(console, "error").mockImplementation(() => {}); + let captured: any; + let currentContext: any; + + const CaptureComponent = () => { + const { resetContext, context } = useABSmartly(); + captured = resetContext; + currentContext = context; + return null; + }; + + render( + + + , + ); + + const original = currentContext; + original.finalize.mockRejectedValueOnce(new Error("publish failed")); + + await act(async () => { + await expect( + captured({ units: { user_id: "new-user" } }), + ).rejects.toThrow("publish failed"); + }); + + const replacement = mockCreateContextWith.mock.results[0].value; + expect(replacement.finalize).toHaveBeenCalledTimes(1); + expect(currentContext).toBe(original); + }); +}); + +describe("Fix #13: withABSmartly HOC null guard", () => { + it("should throw when rendered outside SDKProvider", () => { + vi.spyOn(console, "error").mockImplementation(() => {}); + + interface TestProps { + absmartly: ABSmartly; + } + + const TestComponent = ({ absmartly }: TestProps) => ( +
{absmartly ? "has-context" : "no-context"}
+ ); + + const WrappedComponent = withABSmartly(TestComponent); + + expect(() => render()).toThrow( + "withABSmartly must be used within an SDKProvider", + ); + }); + + it("should work correctly when inside SDKProvider", () => { + interface TestProps { + absmartly: ABSmartly; + } + + const TestComponent = ({ absmartly }: TestProps) => ( +
{absmartly ? "has-context" : "no-context"}
+ ); + + const WrappedComponent = withABSmartly(TestComponent); + + render( + + + , + ); + + expect(screen.getByTestId("result")).toHaveTextContent("has-context"); + }); +}); + +describe("Fix #14: resetContext returns Promise", () => { + it("resetContext should return a promise that can be awaited", async () => { + let capturedResetContext: any; + + const CaptureComponent = () => { + const { resetContext } = useABSmartly(); + capturedResetContext = resetContext; + return
test
; + }; + + render( + + + , + ); + + const result = capturedResetContext({ units: { user_id: "new" } }); + expect(result).toBeInstanceOf(Promise); + await result; + }); +}); diff --git a/tests/fix-plan.test.tsx b/tests/fix-plan.test.tsx new file mode 100644 index 0000000..dd6b351 --- /dev/null +++ b/tests/fix-plan.test.tsx @@ -0,0 +1,470 @@ +import { act, cleanup, render, screen, waitFor } from "@testing-library/react"; +import React from "react"; +import { afterEach, describe, expect, it, vi, MockedClass } from "vitest"; + +import { Context, SDK } from "@absmartly/javascript-sdk"; +import { Treatment, TreatmentVariant, TreatmentProps } from "../src"; +import { TreatmentFunction } from "../src/components/Treatment/TreatmentFunction"; +import { ErrorBoundary } from "../src/components/ErrorBoundary/ErrorBoundary"; +import { mocks } from "./mocks"; + +vi.mock("@absmartly/javascript-sdk"); +vi.mock("../src/hooks/useABSmartly"); + +afterEach(() => { + cleanup(); + vi.resetAllMocks(); + vi.restoreAllMocks(); + vi.unstubAllEnvs(); + mocks.context.variableKeys.mockReturnValue({ "button.color": "red" }); + mocks.context.isFailed.mockReturnValue(false); +}); + +describe("Fix #1: Rules of Hooks - Treatment renders without context", () => { + it("should return null when no context prop is provided and no provider", () => { + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + + const { container } = render( + + +

Control

+
+
, + ); + + expect(container.innerHTML).toBe(""); + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining("No context available"), + ); + warnSpy.mockRestore(); + }); + + it("should not crash with hooks error when context is missing", () => { + vi.spyOn(console, "warn").mockImplementation(() => {}); + + expect(() => + render( + + +

Control

+
+
, + ), + ).not.toThrow(); + }); +}); + +describe("Fix #1: Rules of Hooks - TreatmentFunction renders without context", () => { + it("should return null when no context is available", () => { + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + + const { container } = render( + + {({ variant }: TreatmentProps) =>

Variant {variant}

} +
, + ); + + expect(container.innerHTML).toBe(""); + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining("No context available"), + ); + warnSpy.mockRestore(); + }); + + it("should not crash with hooks error when context is missing", () => { + vi.spyOn(console, "warn").mockImplementation(() => {}); + + expect(() => + render( + + {({ variant }: TreatmentProps) =>

Variant {variant}

} +
, + ), + ).not.toThrow(); + }); +}); + +describe("Fix #3: useContextReady calls onReady when initially ready", () => { + it("should call treatment when context is ready on mount", async () => { + mocks.context.isReady.mockReturnValue(true); + mocks.context.treatment.mockReturnValue(1); + mocks.context.peek.mockReturnValue(1); + + render( + + +

Control

+
+ +

Variant 1

+
+
, + ); + + await waitFor(() => { + expect(mocks.context.treatment).toHaveBeenCalledWith("test_exp"); + }); + }); + + it("should set correct variant when context is ready on mount", async () => { + mocks.context.isReady.mockReturnValue(true); + mocks.context.treatment.mockReturnValue(1); + mocks.context.peek.mockReturnValue(1); + + const { getByText } = render( + + +

Control

+
+ +

Variant 1

+
+
, + ); + + await waitFor(() => { + expect(getByText("Variant 1")).toBeTruthy(); + }); + }); +}); + +describe("Fix #4 & #15: Error messages hidden in production", () => { + it("Treatment should not show error.message in production", async () => { + vi.stubEnv("NODE_ENV", "production"); + + mocks.context.isReady.mockReturnValue(false); + mocks.context.isFailed.mockReturnValue(true); + mocks.context.ready.mockRejectedValue(new Error("Secret API error")); + + vi.spyOn(console, "error").mockImplementation(() => {}); + + const { container } = render( + + +

Control

+
+
, + ); + + await waitFor(() => { + const alert = container.querySelector('[role="alert"]'); + expect(alert).not.toBeNull(); + expect(alert!.textContent).not.toContain("Secret API error"); + }); + }); + + it("Treatment should show error.message in development", async () => { + vi.stubEnv("NODE_ENV", "development"); + + mocks.context.isReady.mockReturnValue(false); + mocks.context.isFailed.mockReturnValue(true); + mocks.context.ready.mockRejectedValue(new Error("Debug error info")); + + vi.spyOn(console, "error").mockImplementation(() => {}); + + const { container } = render( + + +

Control

+
+
, + ); + + await waitFor(() => { + const alert = container.querySelector('[role="alert"]'); + expect(alert).not.toBeNull(); + expect(alert!.textContent).toContain("Debug error info"); + }); + }); + + it("TreatmentFunction should not show error.message in production", async () => { + vi.stubEnv("NODE_ENV", "production"); + + mocks.context.isReady.mockReturnValue(false); + mocks.context.isFailed.mockReturnValue(true); + mocks.context.ready.mockRejectedValue(new Error("Secret error")); + + vi.spyOn(console, "error").mockImplementation(() => {}); + + const { container } = render( + + {({ variant }: TreatmentProps) =>

Variant {variant}

} +
, + ); + + await waitFor(() => { + const alert = container.querySelector('[role="alert"]'); + expect(alert).not.toBeNull(); + expect(alert!.textContent).not.toContain("Secret error"); + }); + }); + + it("ErrorBoundary should show generic message in production", () => { + vi.stubEnv("NODE_ENV", "production"); + + vi.spyOn(console, "error").mockImplementation(() => {}); + + const ThrowingComponent = () => { + throw new Error("Internal secret"); + }; + + const { container } = render( + + + , + ); + + expect(container.textContent).toContain("Something went wrong."); + expect(container.textContent).not.toContain("Internal secret"); + }); + + it("ErrorBoundary should show error message in development", () => { + vi.stubEnv("NODE_ENV", "development"); + + vi.spyOn(console, "error").mockImplementation(() => {}); + + const ThrowingComponent = () => { + throw new Error("Debug details here"); + }; + + const { container } = render( + + + , + ); + + expect(container.textContent).toContain("Debug details here"); + }); +}); + +describe("Fix #8: Treatment accepts errorComponent prop", () => { + it("should render custom errorComponent when error occurs", async () => { + mocks.context.isReady.mockReturnValue(false); + mocks.context.isFailed.mockReturnValue(true); + mocks.context.ready.mockRejectedValue(new Error("Test error")); + + vi.spyOn(console, "error").mockImplementation(() => {}); + + const { queryByTestId } = render( + Custom Error UI} + > + +

Control

+
+
, + ); + + await waitFor(() => { + const customError = queryByTestId("custom-error"); + expect(customError).not.toBeNull(); + expect(customError!.textContent).toBe("Custom Error UI"); + }); + }); +}); + +describe("Fix #10: childrenInfo memoized in Treatment", () => { + it("should render correct variant with memoized childrenInfo", async () => { + mocks.context.isReady.mockReturnValue(true); + mocks.context.treatment.mockReturnValue(2); + mocks.context.peek.mockReturnValue(2); + + const { getByText } = render( + + +

Control

+
+ +

Variant 1

+
+ +

Variant 2

+
+
, + ); + + await waitFor(() => { + expect(getByText("Variant 2")).toBeTruthy(); + }); + }); +}); + +describe("Fix #11: useContextReady uses refs for onReady and attributes", () => { + it("keeps SDK calls bounded across rerenders with inline attributes", async () => { + mocks.context.isReady.mockReturnValue(true); + mocks.context.treatment.mockReturnValue(0); + mocks.context.peek.mockReturnValue(0); + + const TestComponent = () => ( + + +

Control

+
+
+ ); + + const { rerender } = render(); + await screen.findByText("Control"); + + for (let i = 0; i < 5; i++) { + rerender(); + } + + expect(mocks.context.attributes).toHaveBeenCalledWith({ key: "value" }); + expect(mocks.context.attributes).toHaveBeenCalledTimes(2); + expect(mocks.context.treatment).toHaveBeenCalledWith("test_exp"); + expect(mocks.context.treatment).toHaveBeenCalledTimes(1); + }); +}); + +describe("Treatment rerender while context is finalizing", () => { + it("does not evaluate a finalizing context when children change", async () => { + let finalizing = false; + const ctx = { + isReady: vi.fn().mockReturnValue(true), + isFailed: vi.fn().mockReturnValue(false), + isFinalizing: vi.fn(() => finalizing), + isFinalized: vi.fn().mockReturnValue(false), + ready: vi.fn().mockResolvedValue(undefined), + attributes: vi.fn(), + treatment: vi.fn().mockReturnValue(0), + peek: vi.fn(() => { + if (finalizing) throw new Error("ABsmartly Context is finalizing."); + return 0; + }), + } as unknown as Context; + + vi.spyOn(console, "error").mockImplementation(() => {}); + + const renderTree = (label: string) => ( + Boundary fallback

}> + + +

{label}

+
+
+
+ ); + + const { rerender } = render(renderTree("Control A")); + await waitFor(() => expect(screen.getByText("Control A")).toBeTruthy()); + + finalizing = true; + rerender(renderTree("Control B")); + + await waitFor(() => expect(screen.getByText("Control B")).toBeTruthy()); + expect(screen.queryByText("Boundary fallback")).toBeNull(); + }); +}); + +describe("Declarative treatments during reset", () => { + const makeFinalizingContext = () => { + let finalizing = false; + const fail = () => { + if (finalizing) throw new Error("ABsmartly Context is finalizing."); + return 0; + }; + const ctx = { + isReady: vi.fn().mockReturnValue(true), + isFailed: vi.fn().mockReturnValue(false), + isFinalizing: vi.fn(() => finalizing), + isFinalized: vi.fn().mockReturnValue(false), + ready: vi.fn().mockResolvedValue(undefined), + attributes: vi.fn(), + treatment: vi.fn(fail), + peek: vi.fn(fail), + variableKeys: vi.fn(() => { + fail(); + return {}; + }), + peekVariableValue: vi.fn(), + } as unknown as Context; + return { ctx, startFinalizing: () => (finalizing = true) }; + }; + + it("Treatment name change while finalizing reports an error instead of throwing", async () => { + vi.spyOn(console, "error").mockImplementation(() => {}); + const { ctx, startFinalizing } = makeFinalizingContext(); + const tree = (name: string) => ( + Boundary fallback

}> + + +

Control

+
+
+
+ ); + + const { rerender } = render(tree("exp_a")); + await waitFor(() => expect(screen.getByText("Control")).toBeTruthy()); + + startFinalizing(); + rerender(tree("exp_b")); + + await waitFor(() => expect(screen.getByRole("alert")).toBeTruthy()); + expect(screen.queryByText("Boundary fallback")).toBeNull(); + }); + + it("TreatmentFunction name change while finalizing reports an error instead of throwing", async () => { + vi.spyOn(console, "error").mockImplementation(() => {}); + const { ctx, startFinalizing } = makeFinalizingContext(); + const tree = (name: string) => ( + Boundary fallback

}> + + {({ variant }: TreatmentProps) =>

Variant {variant}

} +
+
+ ); + + const { rerender } = render(tree("exp_a")); + await waitFor(() => expect(screen.getByText("Variant 0")).toBeTruthy()); + + startFinalizing(); + rerender(tree("exp_b")); + + await waitFor(() => expect(screen.getByRole("alert")).toBeTruthy()); + expect(screen.queryByText("Boundary fallback")).toBeNull(); + }); + + it("Treatment mounted while finalizing reports an error instead of throwing", async () => { + vi.spyOn(console, "error").mockImplementation(() => {}); + const { ctx, startFinalizing } = makeFinalizingContext(); + startFinalizing(); + + render( + Boundary fallback

}> + + +

Control

+
+
+
, + ); + + await waitFor(() => expect(screen.getByRole("alert")).toBeTruthy()); + expect(screen.queryByText("Boundary fallback")).toBeNull(); + }); + + it("TreatmentFunction mounted while finalizing reports an error instead of throwing", async () => { + vi.spyOn(console, "error").mockImplementation(() => {}); + const { ctx, startFinalizing } = makeFinalizingContext(); + startFinalizing(); + + render( + Boundary fallback

}> + + {({ variant }: TreatmentProps) =>

Variant {variant}

} +
+
, + ); + + await waitFor(() => expect(screen.getByRole("alert")).toBeTruthy()); + expect(screen.queryByText("Boundary fallback")).toBeNull(); + }); +}); diff --git a/tests/integration.test.tsx b/tests/integration.test.tsx new file mode 100644 index 0000000..167dbf8 --- /dev/null +++ b/tests/integration.test.tsx @@ -0,0 +1,296 @@ +import { cleanup, render, screen, waitFor } from "@testing-library/react"; +import React, { Suspense, Component, ReactNode } from "react"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { Context } from "@absmartly/javascript-sdk"; + +import { + Treatment, + TreatmentVariant, + TreatmentFunction, + TreatmentProps, +} from "../src"; + +vi.mock("@absmartly/javascript-sdk"); + +afterEach(() => { + cleanup(); + vi.clearAllMocks(); +}); + +const mockContextData = { + experiments: [], +}; + +const createMockContext = (treatment: number, isReady = true) => ({ + data: vi.fn().mockReturnValue(mockContextData), + isReady: vi.fn().mockReturnValue(isReady), + isFailed: vi.fn().mockReturnValue(false), + treatment: vi.fn().mockReturnValue(treatment), + peek: vi.fn().mockReturnValue(treatment), + ready: vi.fn().mockResolvedValue(undefined), + attributes: vi.fn(), + variableKeys: vi.fn().mockReturnValue({}), + peekVariableValue: vi.fn(), +}); + +describe("Integration Tests", () => { + describe("Multiple Treatments in Tree", () => { + it("should render multiple treatments correctly in a component tree", async () => { + const context1 = createMockContext(1); + const context2 = createMockContext(2); + const context3 = createMockContext(0); + + render( +
+ + +
Header Control
+
+ +
Header Variant
+
+
+ + + + + + + + + + + + + + + +
Simple Footer
+
+ +
Extended Footer
+
+
+
, + ); + + await waitFor(() => { + expect(screen.getByTestId("header-variant")).toBeInTheDocument(); + expect(screen.getByTestId("button-variant2")).toBeInTheDocument(); + expect(screen.getByTestId("footer-control")).toBeInTheDocument(); + }); + + expect(screen.queryByTestId("header-control")).not.toBeInTheDocument(); + expect(screen.queryByTestId("button-control")).not.toBeInTheDocument(); + expect(screen.queryByTestId("footer-variant")).not.toBeInTheDocument(); + }); + + it("should handle nested treatments correctly", async () => { + const outerContext = createMockContext(1); + const innerContext = createMockContext(0); + + render( + + +
Outer Control
+
+ +
+ Outer Variant + + + Inner Control + + + Inner Variant + + +
+
+
, + ); + + await waitFor(() => { + expect(screen.getByTestId("outer-variant")).toBeInTheDocument(); + expect(screen.getByTestId("inner-control")).toBeInTheDocument(); + }); + + expect(screen.queryByTestId("outer-control")).not.toBeInTheDocument(); + expect(screen.queryByTestId("inner-variant")).not.toBeInTheDocument(); + }); + }); + + describe("Treatment with Suspense", () => { + it("should work correctly with React Suspense boundaries", async () => { + const context = createMockContext(1); + + render( + Loading...} + > + + +
Control Content
+
+ +
Variant Content
+
+
+
, + ); + + await waitFor(() => { + expect(screen.getByTestId("variant")).toBeInTheDocument(); + }); + }); + }); + + describe("Treatment with Error Boundary", () => { + class ErrorBoundary extends Component< + { children: ReactNode; fallback: ReactNode }, + { hasError: boolean } + > { + constructor(props: { children: ReactNode; fallback: ReactNode }) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError() { + return { hasError: true }; + } + + render() { + if (this.state.hasError) { + return this.props.fallback; + } + return this.props.children; + } + } + + it("should work correctly inside an error boundary", async () => { + const context = createMockContext(1); + + render( + Error occurred} + > + + +
Control
+
+ +
Variant
+
+
+
, + ); + + await waitFor(() => { + expect(screen.getByTestId("variant")).toBeInTheDocument(); + }); + + expect(screen.queryByTestId("error-fallback")).not.toBeInTheDocument(); + }); + }); + + describe("TreatmentFunction integration", () => { + it("should render correctly with function children pattern", async () => { + const context = createMockContext(2); + + render( + + {({ variant }: TreatmentProps) => ( +
+ {variant === 0 && Basic: $9.99} + {variant === 1 && Pro: $19.99} + {variant === 2 && Enterprise: $49.99} +
+ )} +
, + ); + + await waitFor(() => { + expect(screen.getByTestId("pricing")).toHaveTextContent( + "Enterprise: $49.99", + ); + }); + }); + + it("should pass variant and variables to render function", async () => { + const context = createMockContext(1); + + render( + + {({ variant, variables }: TreatmentProps) => ( +
+ {variant} + {typeof variables} +
+ )} +
, + ); + + await waitFor(() => { + expect(screen.getByTestId("variant-value")).toHaveTextContent("1"); + expect(screen.getByTestId("variables-type")).toHaveTextContent( + "object", + ); + }); + }); + }); + + describe("Treatment with attributes", () => { + it("should pass attributes to context correctly", async () => { + const context = createMockContext(1); + const attributes = { plan: "premium", country: "US" }; + + render( + + +
Control
+
+ +
Personalized Content
+
+
, + ); + + await waitFor(() => { + expect(context.attributes).toHaveBeenCalledWith(attributes); + expect(screen.getByTestId("personalized")).toBeInTheDocument(); + }); + }); + }); +}); diff --git a/tests/mocks.ts b/tests/mocks.ts index 996e2ae..3a5279b 100644 --- a/tests/mocks.ts +++ b/tests/mocks.ts @@ -1,10 +1,11 @@ import { Context } from "@absmartly/javascript-sdk"; import { Mocked, MockedFunction, vi } from "vitest"; -import { useABSmartly } from "../src/hooks/useABSmartly"; +import { useABSmartly, useOptionalABSmartly } from "../src/hooks/useABSmartly"; export const mocks = { context: { treatment: vi.fn(), + peek: vi.fn(), experimentConfig: vi.fn(), attributes: vi.fn(), ready: vi.fn(), @@ -18,3 +19,6 @@ export const mocks = { export const mockedUseABSmartly = useABSmartly as MockedFunction< typeof useABSmartly >; + +export const mockedUseOptionalABSmartly = + useOptionalABSmartly as MockedFunction; diff --git a/tests/ssr.test.tsx b/tests/ssr.test.tsx new file mode 100644 index 0000000..727f526 --- /dev/null +++ b/tests/ssr.test.tsx @@ -0,0 +1,191 @@ +import { cleanup, renderHook } from "@testing-library/react"; +import React from "react"; +import { + afterEach, + beforeEach, + describe, + expect, + it, + vi, + MockedFunction, +} from "vitest"; +import { renderToString } from "react-dom/server"; + +import { Context, SDK } from "@absmartly/javascript-sdk"; +import { + useABSmartly, + useTreatment, + Treatment, + TreatmentVariant, +} from "../src"; + +vi.mock("../src/hooks/useABSmartly"); + +const mockedUseABSmartly = useABSmartly as MockedFunction; + +const createMockContext = (isReady = true) => ({ + ready: vi.fn().mockResolvedValue(undefined), + peek: vi.fn().mockReturnValue(1), + treatment: vi.fn().mockReturnValue(1), + isReady: vi.fn().mockReturnValue(isReady), + isFailed: vi.fn().mockReturnValue(false), + attributes: vi.fn(), +}); + +afterEach(() => { + cleanup(); + vi.clearAllMocks(); +}); + +describe("SSR Support", () => { + describe("useTreatment SSR behavior", () => { + it("should return variant immediately when context is pre-ready (SSR scenario)", () => { + const mockContext = createMockContext(true); + mockContext.peek.mockReturnValue(2); + mockContext.treatment.mockReturnValue(2); + + mockedUseABSmartly.mockReturnValue({ + context: mockContext as unknown as Context, + sdk: null as unknown as SDK, + resetContext: vi.fn().mockResolvedValue(undefined), + }); + + const TestComponent = () => { + const { variant, loading, error } = useTreatment("experiment-name"); + return ( +
+ {loading ? "Loading" : `Variant: ${variant}`} + {error ? " error" : ""} +
+ ); + }; + + const html = renderToString(); + + expect(html).toContain("Variant: 2"); + expect(html).not.toContain("Loading"); + expect(html).not.toContain("error"); + expect(mockContext.ready).not.toHaveBeenCalled(); + }); + + it("should support static rendering without async operations when context is ready", () => { + const mockContext = createMockContext(true); + mockContext.peek.mockReturnValue(1); + mockContext.treatment.mockReturnValue(1); + + mockedUseABSmartly.mockReturnValue({ + context: mockContext as unknown as Context, + sdk: null as unknown as SDK, + resetContext: vi.fn().mockResolvedValue(undefined), + }); + + const TestComponent = () => { + const { variant, loading } = useTreatment("test-experiment"); + return
{loading ? "Loading..." : `Variant: ${variant}`}
; + }; + + const html = renderToString(); + + expect(html).toContain("Variant: 1"); + expect(html).not.toContain("Loading..."); + }); + }); + + describe("Treatment component SSR behavior", () => { + it("should render control variant during SSR when context is not ready", () => { + const mockContext = createMockContext(false); + + mockedUseABSmartly.mockReturnValue({ + context: mockContext as unknown as Context, + sdk: null as unknown as SDK, + resetContext: vi.fn().mockResolvedValue(undefined), + }); + + const html = renderToString( + + +
Control
+
+ +
Variant 1
+
+
, + ); + + expect(html).toContain("Control"); + expect(html).not.toContain("Variant 1"); + }); + + it("should render correct variant during SSR when context is ready", () => { + const mockContext = createMockContext(true); + mockContext.treatment.mockReturnValue(1); + + mockedUseABSmartly.mockReturnValue({ + context: mockContext as unknown as Context, + sdk: null as unknown as SDK, + resetContext: vi.fn().mockResolvedValue(undefined), + }); + + const html = renderToString( + + +
Control
+
+ +
Variant 1
+
+
, + ); + + expect(html).toContain("Variant 1"); + expect(html).not.toContain("Control"); + }); + + it("should render loading component during SSR when provided and context not ready", () => { + const mockContext = createMockContext(false); + + mockedUseABSmartly.mockReturnValue({ + context: mockContext as unknown as Context, + sdk: null as unknown as SDK, + resetContext: vi.fn().mockResolvedValue(undefined), + }); + + const html = renderToString( + Loading...} + > + +
Control
+
+ +
Variant 1
+
+
, + ); + + expect(html).toContain("Loading..."); + expect(html).not.toContain("Control"); + expect(html).not.toContain("Variant 1"); + }); + }); + + describe("Context serialization for SSR", () => { + it("should allow context data to be accessed synchronously", () => { + const mockContext = createMockContext(true); + mockContext.treatment.mockReturnValue(0); + + mockedUseABSmartly.mockReturnValue({ + context: mockContext as unknown as Context, + sdk: null as unknown as SDK, + resetContext: vi.fn().mockResolvedValue(undefined), + }); + + const { result } = renderHook(() => useTreatment("experiment")); + + expect(result.current.context).toBeDefined(); + expect(mockContext.treatment).toHaveBeenCalledWith("experiment"); + }); + }); +}); diff --git a/tests/useTreatment.test.ts b/tests/useTreatment.test.ts index 16f29da..91e76af 100644 --- a/tests/useTreatment.test.ts +++ b/tests/useTreatment.test.ts @@ -16,17 +16,21 @@ vi.mock("../src/hooks/useABSmartly"); const mockedUseABSmartly = useABSmartly as MockedFunction; -const mockContext = { +const createMockContext = () => ({ ready: vi.fn(), peek: vi.fn(), treatment: vi.fn(), -}; + isReady: vi.fn().mockReturnValue(false), +}); + +let mockContext = createMockContext(); beforeEach(() => { + mockContext = createMockContext(); mockedUseABSmartly.mockReturnValue({ context: mockContext as unknown as Context, sdk: null as unknown as SDK, - resetContext: () => {}, + resetContext: vi.fn().mockResolvedValue(undefined), }); }); @@ -53,6 +57,25 @@ describe("useTreatment", () => { }); }); + it("evaluates once after asynchronous readiness", async () => { + let resolveReady!: () => void; + mockContext.ready.mockReturnValueOnce( + new Promise((resolve) => { + resolveReady = () => { + mockContext.isReady.mockReturnValue(true); + resolve(); + }; + }), + ); + mockContext.treatment.mockReturnValue(1); + + const { result } = renderHook(() => useTreatment("experiment-name")); + resolveReady(); + + await waitFor(() => expect(result.current.variant).toBe(1)); + expect(mockContext.treatment).toHaveBeenCalledTimes(1); + }); + it("should handle errors correctly", async () => { const error = new Error("Test error"); mockContext.ready.mockRejectedValueOnce(error); @@ -90,4 +113,155 @@ describe("useTreatment", () => { expect(result.current.error?.message).toBe("Test error"); }); }); + + it("should return context object for direct access", async () => { + mockContext.ready.mockResolvedValueOnce(undefined); + mockContext.treatment.mockReturnValueOnce(1); + + const { result } = renderHook(() => useTreatment("experiment-name")); + + await waitFor(() => { + expect(result.current.context).toBeDefined(); + expect(result.current.context).toBe(mockContext); + }); + }); + + it("should return variant immediately when context is already ready", () => { + mockContext.isReady.mockReturnValue(true); + mockContext.treatment.mockReturnValue(2); + + const { result } = renderHook(() => useTreatment("experiment-name")); + + expect(result.current.loading).toBe(false); + expect(result.current.variant).toBe(2); + expect(result.current.error).toBe(null); + expect(mockContext.ready).not.toHaveBeenCalled(); + }); + + it("should use peek immediately when context is already ready and peek is true", () => { + mockContext.isReady.mockReturnValue(true); + mockContext.peek.mockReturnValue(3); + + const { result } = renderHook(() => useTreatment("experiment-name", true)); + + expect(result.current.loading).toBe(false); + expect(result.current.variant).toBe(3); + expect(mockContext.peek).toHaveBeenCalledWith("experiment-name"); + expect(mockContext.treatment).not.toHaveBeenCalled(); + }); + + it("should update variant when experiment name changes", async () => { + mockContext.ready.mockResolvedValue(undefined); + mockContext.treatment.mockReturnValueOnce(1).mockReturnValueOnce(2); + + const { result, rerender } = renderHook(({ name }) => useTreatment(name), { + initialProps: { name: "experiment-1" }, + }); + + await waitFor(() => { + expect(result.current.variant).toBe(1); + }); + + rerender({ name: "experiment-2" }); + + await waitFor(() => { + expect(result.current.variant).toBe(2); + }); + }); + + it("should handle variant 0 correctly (falsy but valid)", async () => { + mockContext.ready.mockResolvedValueOnce(undefined); + mockContext.treatment.mockReturnValueOnce(0); + + const { result } = renderHook(() => useTreatment("experiment-name")); + + await waitFor(() => { + expect(result.current.loading).toBe(false); + expect(result.current.variant).toBe(0); + expect(result.current.error).toBe(null); + }); + }); + + it("should not skip effect when context changes but isReady is still false", async () => { + const firstContext = createMockContext(); + const secondContext = createMockContext(); + + firstContext.ready.mockResolvedValue(undefined); + firstContext.treatment.mockReturnValue(1); + secondContext.ready.mockResolvedValue(undefined); + secondContext.treatment.mockReturnValue(2); + + mockedUseABSmartly.mockReturnValue({ + context: firstContext as unknown as Context, + sdk: null as unknown as SDK, + resetContext: vi.fn().mockResolvedValue(undefined), + }); + + const { result, rerender } = renderHook(() => + useTreatment("experiment-name"), + ); + + await waitFor(() => { + expect(result.current.variant).toBe(1); + }); + + mockedUseABSmartly.mockReturnValue({ + context: secondContext as unknown as Context, + sdk: null as unknown as SDK, + resetContext: vi.fn().mockResolvedValue(undefined), + }); + + rerender(); + + await waitFor(() => { + expect(result.current.variant).toBe(2); + }); + }); +}); + +describe("useTreatment input change while context is finalizing", () => { + it("returns the evaluation error instead of throwing", async () => { + vi.spyOn(console, "error").mockImplementation(() => {}); + mockContext.isReady.mockReturnValue(true); + mockContext.peek.mockReturnValue(0); + mockContext.treatment.mockReturnValue(0); + + const { result, rerender } = renderHook(({ name }) => useTreatment(name), { + initialProps: { name: "exp_a" }, + }); + expect(result.current.variant).toBe(0); + + mockContext.treatment.mockImplementation(() => { + throw new Error("ABsmartly Context is finalizing."); + }); + + expect(() => rerender({ name: "exp_b" })).not.toThrow(); + await waitFor(() => + expect(result.current.error?.message).toBe( + "ABsmartly Context is finalizing.", + ), + ); + expect(result.current.loading).toBe(false); + }); +}); + +describe("useTreatment mounted while context is finalizing", () => { + it("returns the evaluation error instead of throwing during render", async () => { + vi.spyOn(console, "error").mockImplementation(() => {}); + mockContext.isReady.mockReturnValue(true); + const finalizing = () => { + throw new Error("ABsmartly Context is finalizing."); + }; + mockContext.peek.mockImplementation(finalizing); + mockContext.treatment.mockImplementation(finalizing); + + const { result } = renderHook(() => useTreatment("exp_a")); + + await waitFor(() => + expect(result.current.error?.message).toBe( + "ABsmartly Context is finalizing.", + ), + ); + expect(result.current.loading).toBe(false); + }); }); diff --git a/tests/utils.test.ts b/tests/utils.test.ts new file mode 100644 index 0000000..25687d5 --- /dev/null +++ b/tests/utils.test.ts @@ -0,0 +1,86 @@ +import { describe, expect, it, vi } from "vitest"; +import { convertLetterToNumber } from "../src/utils/convertLetterToNumber"; + +describe("convertLetterToNumber", () => { + describe("number passthrough", () => { + it("should return number as-is when input is a number", () => { + expect(convertLetterToNumber(0)).toBe(0); + expect(convertLetterToNumber(1)).toBe(1); + expect(convertLetterToNumber(5)).toBe(5); + expect(convertLetterToNumber(99)).toBe(99); + }); + }); + + describe("numeric string conversion", () => { + it("should convert numeric strings to numbers", () => { + expect(convertLetterToNumber("0")).toBe(0); + expect(convertLetterToNumber("1")).toBe(1); + expect(convertLetterToNumber("5")).toBe(5); + expect(convertLetterToNumber("9")).toBe(9); + }); + }); + + describe("lowercase letter conversion", () => { + it("should convert lowercase letters to variant numbers (a=0, b=1, etc.)", () => { + expect(convertLetterToNumber("a")).toBe(0); + expect(convertLetterToNumber("b")).toBe(1); + expect(convertLetterToNumber("c")).toBe(2); + expect(convertLetterToNumber("d")).toBe(3); + expect(convertLetterToNumber("e")).toBe(4); + }); + + it("should handle all lowercase letters correctly", () => { + const alphabet = "abcdefghijklmnopqrstuvwxyz"; + for (let i = 0; i < alphabet.length; i++) { + expect(convertLetterToNumber(alphabet[i] as any)).toBe(i); + } + }); + }); + + describe("uppercase letter conversion", () => { + it("should convert uppercase letters to variant numbers (A=0, B=1, etc.)", () => { + expect(convertLetterToNumber("A")).toBe(0); + expect(convertLetterToNumber("B")).toBe(1); + expect(convertLetterToNumber("C")).toBe(2); + expect(convertLetterToNumber("D")).toBe(3); + expect(convertLetterToNumber("E")).toBe(4); + }); + + it("should handle all uppercase letters correctly", () => { + const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + for (let i = 0; i < alphabet.length; i++) { + expect(convertLetterToNumber(alphabet[i] as any)).toBe(i); + } + }); + }); + + describe("edge cases", () => { + it("should handle last letters of alphabet", () => { + expect(convertLetterToNumber("z")).toBe(25); + expect(convertLetterToNumber("Z")).toBe(25); + }); + + it("should handle treatment variant typical values", () => { + expect(convertLetterToNumber("A")).toBe(0); + expect(convertLetterToNumber("B")).toBe(1); + expect(convertLetterToNumber(0)).toBe(0); + expect(convertLetterToNumber(1)).toBe(1); + expect(convertLetterToNumber("0")).toBe(0); + expect(convertLetterToNumber("1")).toBe(1); + }); + }); + + describe("invalid input handling", () => { + it.each(["1junk", "ab", "!", "@", ""])( + "returns 0 and warns for %j", + (input) => { + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + expect(convertLetterToNumber(input)).toBe(0); + expect(warnSpy).toHaveBeenCalledWith( + `convertLetterToNumber: Invalid input "${input}". Returning 0.`, + ); + warnSpy.mockRestore(); + }, + ); + }); +}); diff --git a/tests/withABSmartly.test.tsx b/tests/withABSmartly.test.tsx new file mode 100644 index 0000000..002fb65 --- /dev/null +++ b/tests/withABSmartly.test.tsx @@ -0,0 +1,227 @@ +import { cleanup, render, screen } from "@testing-library/react"; +import React, { ComponentType } from "react"; +import { afterEach, describe, expect, it, vi, MockedClass } from "vitest"; + +import { SDK } from "@absmartly/javascript-sdk"; +import { SDKProvider } from "../src/components/SDKProvider"; +import { withABSmartly } from "../src/hooks/HOCs/withABSmartly"; +import { ABSmartly } from "../src/types"; + +vi.mock("@absmartly/javascript-sdk"); + +afterEach(() => { + cleanup(); + vi.clearAllMocks(); +}); + +const mockContextData = { + experiments: [], +}; + +const mockCreateContext = vi.fn().mockImplementation(function () { + return { + data: vi.fn().mockReturnValue(mockContextData), + isReady: vi.fn().mockReturnValue(true), + isFailed: vi.fn().mockReturnValue(false), + treatment: vi.fn().mockReturnValue(1), + ready: vi.fn().mockResolvedValue(undefined), + attributes: vi.fn(), + variableKeys: vi.fn().mockReturnValue({}), + peekVariableValue: vi.fn(), + finalize: vi.fn().mockResolvedValue(undefined), + }; +}); + +(SDK as MockedClass).mockImplementation(function () { + return { + createContext: mockCreateContext, + createContextWith: vi.fn(), + } as unknown as SDK; +}); + +const sdkOptions = { + endpoint: "https://sandbox.absmartly.io/v1", + apiKey: "test-api-key", + application: "www", + environment: "test", +}; + +const contextOptions = { + units: { + user_id: "test-user-id", + }, +}; + +describe("withABSmartly HOC", () => { + it("should properly wrap a component and pass absmartly prop", () => { + interface TestComponentProps { + absmartly: ABSmartly; + testProp?: string; + } + + const TestComponent = ({ absmartly, testProp }: TestComponentProps) => { + return ( +
+ {absmartly.sdk ? "yes" : "no"} + + {absmartly.context ? "yes" : "no"} + + {testProp || "none"} +
+ ); + }; + + const WrappedComponent = withABSmartly(TestComponent); + + render( + + + , + ); + + expect(screen.getByTestId("test-component")).toBeInTheDocument(); + expect(screen.getByTestId("has-sdk")).toHaveTextContent("yes"); + expect(screen.getByTestId("has-context")).toHaveTextContent("yes"); + }); + + it("should pass through additional props correctly", () => { + interface TestComponentProps { + absmartly: ABSmartly; + message: string; + count: number; + items: string[]; + } + + const TestComponent = ({ message, count, items }: TestComponentProps) => { + return ( +
+ {message} + {count} + {items.join(",")} +
+ ); + }; + + const WrappedComponent = withABSmartly(TestComponent); + + render( + + + , + ); + + expect(screen.getByTestId("message")).toHaveTextContent("test message"); + expect(screen.getByTestId("count")).toHaveTextContent("42"); + expect(screen.getByTestId("items")).toHaveTextContent("a,b,c"); + }); + + it("should provide access to context methods via absmartly prop", () => { + interface TestComponentProps { + absmartly: ABSmartly; + } + + const TestComponent = ({ absmartly }: TestComponentProps) => { + const treatment = absmartly.context.treatment("test_exp"); + return ( +
+ {treatment} + + {typeof absmartly.resetContext === "function" ? "yes" : "no"} + +
+ ); + }; + + const WrappedComponent = withABSmartly(TestComponent); + + render( + + + , + ); + + expect(screen.getByTestId("treatment")).toHaveTextContent("1"); + expect(screen.getByTestId("has-reset")).toHaveTextContent("yes"); + }); + + it("should set correct displayName for wrapped component", () => { + interface TestComponentProps { + absmartly: ABSmartly; + } + + const NamedComponent = ({ absmartly }: TestComponentProps) => { + return
{absmartly ? "wrapped" : "not wrapped"}
; + }; + + const WrappedComponent = withABSmartly(NamedComponent); + + expect(WrappedComponent.displayName).toBe("withABSmartly(NamedComponent)"); + }); + + it("should use Component fallback for displayName when component has no name", () => { + interface TestComponentProps { + absmartly: ABSmartly; + } + + const WrappedComponent = withABSmartly((({ + absmartly, + }: TestComponentProps) => ( +
{absmartly ? "wrapped" : "not wrapped"}
+ )) as ComponentType); + + expect(WrappedComponent.displayName).toBe("withABSmartly(Component)"); + }); + + it("should work with components that have displayName property", () => { + interface TestComponentProps { + absmartly: ABSmartly; + } + + const TestComponent = ({ absmartly }: TestComponentProps) => { + return
{absmartly ? "wrapped" : "not wrapped"}
; + }; + TestComponent.displayName = "CustomDisplayName"; + + const WrappedComponent = withABSmartly(TestComponent); + + expect(WrappedComponent.displayName).toBe( + "withABSmartly(CustomDisplayName)", + ); + }); + + it("should work with class components", () => { + interface TestComponentProps { + absmartly: ABSmartly; + title: string; + } + + class ClassComponent extends React.Component { + render() { + const { absmartly, title } = this.props; + return ( +
+ {title} + + {absmartly.context ? "yes" : "no"} + +
+ ); + } + } + + const WrappedComponent = withABSmartly(ClassComponent); + + render( + + + , + ); + + expect(screen.getByTestId("title")).toHaveTextContent("Test Title"); + expect(screen.getByTestId("has-context")).toHaveTextContent("yes"); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index d60a4c3..b506991 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,10 +1,12 @@ { "compilerOptions": { + "strict": true, + "skipLibCheck": true, "outDir": "lib/esm", "allowJs": true, "module": "esnext", "target": "es5", - "lib": ["es6", "dom", "es2016", "es2017"], + "lib": ["es6", "dom", "es2016", "es2017", "es2019"], "jsx": "react", "declaration": true, "moduleResolution": "node", @@ -12,9 +14,6 @@ "noUnusedParameters": true, "esModuleInterop": true, "noImplicitReturns": true, - "noImplicitThis": true, - "noImplicitAny": true, - "strictNullChecks": true, "suppressImplicitAnyIndexErrors": true, "allowSyntheticDefaultImports": true }, diff --git a/yarn.lock b/yarn.lock index 2fbee69..0f3eb5f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16,14 +16,6 @@ resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.3.3.tgz#90749bde8b89cd41764224f5aac29cd4138f75ff" integrity sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ== -"@ampproject/remapping@^2.2.1": - version "2.3.0" - resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" - integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== - dependencies: - "@jridgewell/gen-mapping" "^0.3.5" - "@jridgewell/trace-mapping" "^0.3.24" - "@babel/code-frame@^7.10.4": version "7.24.6" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.6.tgz#ab88da19344445c3d8889af2216606d3329f3ef2" @@ -32,16 +24,21 @@ "@babel/highlight" "^7.24.6" picocolors "^1.0.0" -"@babel/helper-string-parser@^7.24.6": - version "7.24.6" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.6.tgz#28583c28b15f2a3339cfafafeaad42f9a0e828df" - integrity sha512-WdJjwMEkmBicq5T9fm/cHND3+UlFa2Yj8ALLgmoSQAJZysYbBjw+azChSGPN4DSPLXOcooGRvDwZWMcF/mLO2Q== +"@babel/helper-string-parser@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz#7f0871d99824d23137d60f86fcf6130fd5a1b51f" + integrity sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw== "@babel/helper-validator-identifier@^7.24.6": version "7.24.6" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.6.tgz#08bb6612b11bdec78f3feed3db196da682454a5e" integrity sha512-4yA7s865JHaqUdRbnaxarZREuPTHrjpDT+pXoAZ1yhyo6uFnIEpS8VMu16siFOHDpZNKYv5BObhsB//ycbICyw== +"@babel/helper-validator-identifier@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz#bd87084ced0c796ec46bda492de6e83d29e89fc2" + integrity sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg== + "@babel/highlight@^7.24.6": version "7.24.6" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.6.tgz#6d610c1ebd2c6e061cade0153bf69b0590b7b3df" @@ -52,10 +49,12 @@ js-tokens "^4.0.0" picocolors "^1.0.0" -"@babel/parser@^7.24.4": - version "7.24.6" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.6.tgz#5e030f440c3c6c78d195528c3b688b101a365328" - integrity sha512-eNZXdfU35nJC2h24RznROuOpO94h6x8sg9ju0tT9biNtLZ2vuP8SduLqqV+/8+cebSLV9SJEAN5Z3zQbJG/M+Q== +"@babel/parser@^7.29.7": + version "7.29.9" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.29.9.tgz#e7ee0a24f752c5296c6455855ccb72662e67ea96" + integrity sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA== + dependencies: + "@babel/types" "^7.29.8" "@babel/runtime@^7.12.5", "@babel/runtime@^7.9.2": version "7.24.6" @@ -64,19 +63,18 @@ dependencies: regenerator-runtime "^0.14.0" -"@babel/types@^7.24.0": - version "7.24.6" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.6.tgz#ba4e1f59870c10dc2fa95a274ac4feec23b21912" - integrity sha512-WaMsgi6Q8zMgMth93GvWPXkhAIEobfsIkLTacoVZoK1J0CevIPGYY2Vo5YvJGqyHqXM6P4ppOYGsIRU8MM9pFQ== +"@babel/types@^7.29.7", "@babel/types@^7.29.8": + version "7.29.8" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.29.8.tgz#1229eef31d85156d70fa3f4cd859376d0eaf6863" + integrity sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg== dependencies: - "@babel/helper-string-parser" "^7.24.6" - "@babel/helper-validator-identifier" "^7.24.6" - to-fast-properties "^2.0.0" + "@babel/helper-string-parser" "^7.29.7" + "@babel/helper-validator-identifier" "^7.29.7" -"@bcoe/v8-coverage@^0.2.3": - version "0.2.3" - resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" - integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== +"@bcoe/v8-coverage@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz#bbe12dca5b4ef983a0d0af4b07b9bc90ea0ababa" + integrity sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA== "@cspotcode/source-map-support@^0.8.0": version "0.8.1" @@ -85,157 +83,21 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" -"@esbuild/aix-ppc64@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz#a70f4ac11c6a1dfc18b8bbb13284155d933b9537" - integrity sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g== - -"@esbuild/android-arm64@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz#db1c9202a5bc92ea04c7b6840f1bbe09ebf9e6b9" - integrity sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg== - -"@esbuild/android-arm@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.20.2.tgz#3b488c49aee9d491c2c8f98a909b785870d6e995" - integrity sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w== - -"@esbuild/android-x64@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.20.2.tgz#3b1628029e5576249d2b2d766696e50768449f98" - integrity sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg== - -"@esbuild/darwin-arm64@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz#6e8517a045ddd86ae30c6608c8475ebc0c4000bb" - integrity sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA== - -"@esbuild/darwin-x64@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz#90ed098e1f9dd8a9381695b207e1cff45540a0d0" - integrity sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA== - -"@esbuild/freebsd-arm64@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz#d71502d1ee89a1130327e890364666c760a2a911" - integrity sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw== - -"@esbuild/freebsd-x64@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz#aa5ea58d9c1dd9af688b8b6f63ef0d3d60cea53c" - integrity sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw== - -"@esbuild/linux-arm64@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz#055b63725df678379b0f6db9d0fa85463755b2e5" - integrity sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A== - -"@esbuild/linux-arm@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz#76b3b98cb1f87936fbc37f073efabad49dcd889c" - integrity sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg== - -"@esbuild/linux-ia32@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz#c0e5e787c285264e5dfc7a79f04b8b4eefdad7fa" - integrity sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig== - -"@esbuild/linux-loong64@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz#a6184e62bd7cdc63e0c0448b83801001653219c5" - integrity sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ== - -"@esbuild/linux-mips64el@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz#d08e39ce86f45ef8fc88549d29c62b8acf5649aa" - integrity sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA== - -"@esbuild/linux-ppc64@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz#8d252f0b7756ffd6d1cbde5ea67ff8fd20437f20" - integrity sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg== - -"@esbuild/linux-riscv64@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz#19f6dcdb14409dae607f66ca1181dd4e9db81300" - integrity sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg== - -"@esbuild/linux-s390x@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz#3c830c90f1a5d7dd1473d5595ea4ebb920988685" - integrity sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ== - -"@esbuild/linux-x64@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz#86eca35203afc0d9de0694c64ec0ab0a378f6fff" - integrity sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw== - -"@esbuild/netbsd-x64@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz#e771c8eb0e0f6e1877ffd4220036b98aed5915e6" - integrity sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ== - -"@esbuild/openbsd-x64@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz#9a795ae4b4e37e674f0f4d716f3e226dd7c39baf" - integrity sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ== - -"@esbuild/sunos-x64@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz#7df23b61a497b8ac189def6e25a95673caedb03f" - integrity sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w== - -"@esbuild/win32-arm64@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz#f1ae5abf9ca052ae11c1bc806fb4c0f519bacf90" - integrity sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ== - -"@esbuild/win32-ia32@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz#241fe62c34d8e8461cd708277813e1d0ba55ce23" - integrity sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ== - -"@esbuild/win32-x64@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz#9c907b21e30a52db959ba4f80bb01a0cc403d5cc" - integrity sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ== - -"@istanbuljs/schema@^0.1.2": - version "0.1.3" - resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" - integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== - -"@jest/schemas@^29.6.3": - version "29.6.3" - resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03" - integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA== - dependencies: - "@sinclair/typebox" "^0.27.8" - -"@jridgewell/gen-mapping@^0.3.5": - version "0.3.5" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" - integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== - dependencies: - "@jridgewell/set-array" "^1.2.1" - "@jridgewell/sourcemap-codec" "^1.4.10" - "@jridgewell/trace-mapping" "^0.3.24" - "@jridgewell/resolve-uri@^3.0.3", "@jridgewell/resolve-uri@^3.1.0": version "3.1.2" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== -"@jridgewell/set-array@^1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" - integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== - -"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.4.15": +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": version "1.4.15" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== +"@jridgewell/sourcemap-codec@^1.5.5": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz#f4c663e862f06dc98ca4d453862c46902789a18d" + integrity sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw== + "@jridgewell/trace-mapping@0.3.9": version "0.3.9" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" @@ -244,98 +106,103 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" -"@jridgewell/trace-mapping@^0.3.23", "@jridgewell/trace-mapping@^0.3.24": - version "0.3.25" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" - integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== +"@jridgewell/trace-mapping@^0.3.31": + version "0.3.31" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0" + integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== dependencies: "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@rollup/rollup-android-arm-eabi@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz#bbd0e616b2078cd2d68afc9824d1fadb2f2ffd27" - integrity sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ== - -"@rollup/rollup-android-arm64@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz#97255ef6384c5f73f4800c0de91f5f6518e21203" - integrity sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA== - -"@rollup/rollup-darwin-arm64@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz#b6dd74e117510dfe94541646067b0545b42ff096" - integrity sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w== - -"@rollup/rollup-darwin-x64@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz#e07d76de1cec987673e7f3d48ccb8e106d42c05c" - integrity sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA== - -"@rollup/rollup-linux-arm-gnueabihf@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz#9f1a6d218b560c9d75185af4b8bb42f9f24736b8" - integrity sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA== - -"@rollup/rollup-linux-arm-musleabihf@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz#53618b92e6ffb642c7b620e6e528446511330549" - integrity sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A== - -"@rollup/rollup-linux-arm64-gnu@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz#99a7ba5e719d4f053761a698f7b52291cefba577" - integrity sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw== - -"@rollup/rollup-linux-arm64-musl@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz#f53db99a45d9bc00ce94db8a35efa7c3c144a58c" - integrity sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ== - -"@rollup/rollup-linux-powerpc64le-gnu@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz#cbb0837408fe081ce3435cf3730e090febafc9bf" - integrity sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA== - -"@rollup/rollup-linux-riscv64-gnu@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz#8ed09c1d1262ada4c38d791a28ae0fea28b80cc9" - integrity sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg== - -"@rollup/rollup-linux-s390x-gnu@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz#938138d3c8e0c96f022252a28441dcfb17afd7ec" - integrity sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg== - -"@rollup/rollup-linux-x64-gnu@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz#1a7481137a54740bee1ded4ae5752450f155d942" - integrity sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w== - -"@rollup/rollup-linux-x64-musl@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz#f1186afc601ac4f4fc25fac4ca15ecbee3a1874d" - integrity sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg== - -"@rollup/rollup-win32-arm64-msvc@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz#ed6603e93636a96203c6915be4117245c1bd2daf" - integrity sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA== - -"@rollup/rollup-win32-ia32-msvc@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz#14e0b404b1c25ebe6157a15edb9c46959ba74c54" - integrity sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg== - -"@rollup/rollup-win32-x64-msvc@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz#5d694d345ce36b6ecf657349e03eb87297e68da4" - integrity sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g== - -"@sinclair/typebox@^0.27.8": - version "0.27.8" - resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" - integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== +"@oxc-project/types@=0.154.0": + version "0.154.0" + resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.154.0.tgz#0a4f2adaeef41d1931861adca8166b7615e5a49a" + integrity sha512-KYOUGQFlLWR+s4QMV2Q9ko38TZm7EdO87Qm7rvZLqk/webse/cDjtyxKhg/47l0byCbs/k46LT8qc4PKjAikzA== + +"@rolldown/binding-android-arm-eabi@1.2.14": + version "1.2.14" + resolved "https://registry.yarnpkg.com/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.14.tgz#4e56a59bff8b2756fb6474ed4af7fcb1f19e96ed" + integrity sha512-aWR+XKH9+rW7m5fWq/5360J3mHycrx/o6xZM2DQFdJnTMdyP7/WzxPjhtLbVWdKo2y612/4U1bWqb3HQhiF7vg== + +"@rolldown/binding-android-arm64@1.2.14": + version "1.2.14" + resolved "https://registry.yarnpkg.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.14.tgz#71988681aae082ae949e593037fbb331577c5078" + integrity sha512-QiVxHofl6A8lQU7WAtOUY4QMVZXohxwz7WJJoeh8NBEUyKfIkMRKhYyq8az/1RypNiIXzNTtiS6KwHz0erxYRQ== + +"@rolldown/binding-darwin-arm64@1.2.14": + version "1.2.14" + resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.14.tgz#33fcab8a34317f9022b8ae8753ff77289c084116" + integrity sha512-lTQOzHuBIooh3jIr5XINQkjPGEuGy+Dsy0meXDr6Sjty0b99kYgGh4uBC/8mJX1T0kC8d6S0aPP1Y+bHkh6aBw== + +"@rolldown/binding-darwin-x64@1.2.14": + version "1.2.14" + resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.14.tgz#3503ef66841b9a55e97c4f91618eb5583f03686c" + integrity sha512-hyMKEGatjmBYCs/Q4sbIcL/GqpFEqsX3wc+HYVQ/85A4rxc1o7rPm4DUI4GV4k/h0ITddwFSGpqQunI14GXjTA== + +"@rolldown/binding-freebsd-x64@1.2.14": + version "1.2.14" + resolved "https://registry.yarnpkg.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.14.tgz#1bbe60ec663fd61637d2f419fccb4fad734c5f10" + integrity sha512-u371GbXoVXrEgP20PwzoGNOdAOqAMhzo19JzzY+V4YmmnC+DTOoqdbMu+O+54emM59Mik/UdrtWgcuEDKWRPKA== + +"@rolldown/binding-linux-arm-gnueabihf@1.2.14": + version "1.2.14" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.14.tgz#e371df969ce60cac6d439309d2040c49392f3aa6" + integrity sha512-BqwKsdZQf6EqVWr8fv2MrDYzAvOZ7+NYFSJESRGs87fkseEhs0TU9TXgRJTkZoRRkxTizyEg1LKUXfU3/78uqg== + +"@rolldown/binding-linux-arm64-gnu@1.2.14": + version "1.2.14" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.14.tgz#173abeead293d60b3002ef8714978040af0f47b3" + integrity sha512-SD9kY8M009x/7s57Z1dgUVlP5+uYBgKQK0Tkr1WIWngrtUJmRCW1RJFMY7QDla2khXhmg2i5Io7MUjcmZNZ/KQ== + +"@rolldown/binding-linux-arm64-musl@1.2.14": + version "1.2.14" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.14.tgz#8f3133a19e603296b19e5899e05ddcaa5e839d7b" + integrity sha512-BJ2CL19M6uuWesMgnqJzgxoI+N+eV7b0A9O9vLV42y+gitC41fNa8KElau1tHVzuLWX5GzEuxMPhAO0tQXQMkg== + +"@rolldown/binding-linux-ppc64-gnu@1.2.14": + version "1.2.14" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.14.tgz#c1625cd699e119a8f7b3ff98c56ac3e27fc7056d" + integrity sha512-4+Kl8daPV8NDHaD7mfCcc1pRM+Qg7ab8DyMm5FG5WoZgv4h2i3KFN1CybYBNbQqmuYm7ftJLvmMI1eHYJ22S/Q== + +"@rolldown/binding-linux-s390x-gnu@1.2.14": + version "1.2.14" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.14.tgz#b9e48c9f00289a2df6f8e1fad7448e6c643ad52d" + integrity sha512-V/h8ePq1fKDrmtFMz6xcCN3DKvuvMRrwwD1q0PkBeeo4DpK64biaKvqWGVxqCeYKqeRk7gE7/iKPxLCn08O97A== + +"@rolldown/binding-linux-x64-gnu@1.2.14": + version "1.2.14" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.14.tgz#9326add90ba3219102c151e1274fa35eef17dc3b" + integrity sha512-WxalXzgrLVpKC93xfpaJ3gCXUWzeCr+FRCbDVD2mUAy4L6Vo3lNVz0epUEUpxPWJkLNYUwM4OSIikv36OJ9mLg== + +"@rolldown/binding-linux-x64-musl@1.2.14": + version "1.2.14" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.14.tgz#8e11f27cb56a92276fcabd6f7accbef7089160b3" + integrity sha512-3G3tnAmTpdk9jO3GQtyHXqeFD2SlecfTws/Y3M2mmj9nMBwwuLT4kur8sZKeu7ENUHSpVO3cIsm5A52LEWWwlg== + +"@rolldown/binding-openharmony-arm64@1.2.14": + version "1.2.14" + resolved "https://registry.yarnpkg.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.14.tgz#784873879d9f655de52b5ce1085879a3471d0fd8" + integrity sha512-qyr5XiNGxm2ytx17vvtcJ9nXn6v1gENETi503bJx3OGwZ1tSonj7BosxbMa/xeAfSl5iS9X6KjEWYXLu6v1L7Q== + +"@rolldown/binding-win32-arm64-msvc@1.2.14": + version "1.2.14" + resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.14.tgz#6a656838c13a51defad46592ee613ec2c27ae49f" + integrity sha512-oA4o5o0S2yB199YyYXDzKUv1Z56Rc+mgOjwm7Z/vJ9Z8igiDPYvoOuDyo6g7AK/ZaF09nPhDKVnGBcYHO11GXA== + +"@rolldown/binding-win32-x64-msvc@1.2.14": + version "1.2.14" + resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.14.tgz#f1a24048db1677c1d985a52c922e88c5dbe75e0d" + integrity sha512-9mE94bf2GZUaR/fUSXCMpBoWkctu421Mi0ORpm4zqfbkjKep7RxzPFa5etcGs5h+2KApoWBOA/hAJa/7saGgEQ== + +"@rolldown/pluginutils@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz#e3fcee093fbb5ce765e1ad088ff4de2889f6f9be" + integrity sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw== + +"@standard-schema/spec@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@standard-schema/spec/-/spec-1.1.0.tgz#a79b55dbaf8604812f52d140b2c9ab41bc150bb8" + integrity sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w== "@testing-library/dom@^8.5.0": version "8.20.1" @@ -404,12 +271,25 @@ resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.4.tgz#1a31c3d378850d2778dabb6374d036dcba4ba708" integrity sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw== +"@types/chai@^5.2.2": + version "5.2.3" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-5.2.3.tgz#8e9cd9e1c3581fa6b341a5aed5588eb285be0b4a" + integrity sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA== + dependencies: + "@types/deep-eql" "*" + assertion-error "^2.0.1" + "@types/core-js@^2.5.5": version "2.5.8" resolved "https://registry.yarnpkg.com/@types/core-js/-/core-js-2.5.8.tgz#d5c6ec44f2f3328653dce385ae586bd8261f8e85" integrity sha512-VgnAj6tIAhJhZdJ8/IpxdatM8G4OD3VWGlp6xIxUGENZlpbob9Ty4VVdC1FIEp0aK6DBscDDjyzy5FB60TuNqg== -"@types/estree@1.0.5", "@types/estree@^1.0.0": +"@types/deep-eql@*": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/deep-eql/-/deep-eql-4.0.2.tgz#334311971d3a07121e7eb91b684a605e7eea9cbd" + integrity sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw== + +"@types/estree@^1.0.0": version "1.0.5" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== @@ -434,75 +314,88 @@ "@types/prop-types" "*" csstype "^3.0.2" -"@vitest/coverage-v8@^1.6.0": - version "1.6.0" - resolved "https://registry.yarnpkg.com/@vitest/coverage-v8/-/coverage-v8-1.6.0.tgz#2f54ccf4c2d9f23a71294aba7f95b3d2e27d14e7" - integrity sha512-KvapcbMY/8GYIG0rlwwOKCVNRc0OL20rrhFkg/CHNzncV03TE2XWvO5w9uZYoxNiMEBacAJt3unSOiZ7svePew== +"@vitest/coverage-v8@^4.0.18": + version "4.1.12" + resolved "https://registry.yarnpkg.com/@vitest/coverage-v8/-/coverage-v8-4.1.12.tgz#d4633b3cb3fddad0f671689883cd5f8dcdc83e91" + integrity sha512-6S7/iVknKglLAxMFxQGNppBI0iqJS2NgM6FBxavitj7KaDq/vywOWGf6dGu7F2L1Qy0G6S3uTp7aGshHrzK8JA== dependencies: - "@ampproject/remapping" "^2.2.1" - "@bcoe/v8-coverage" "^0.2.3" - debug "^4.3.4" + "@bcoe/v8-coverage" "^1.0.2" + "@vitest/utils" "4.1.12" + ast-v8-to-istanbul "^1.0.0" istanbul-lib-coverage "^3.2.2" istanbul-lib-report "^3.0.1" - istanbul-lib-source-maps "^5.0.4" - istanbul-reports "^3.1.6" - magic-string "^0.30.5" - magicast "^0.3.3" - picocolors "^1.0.0" - std-env "^3.5.0" - strip-literal "^2.0.0" - test-exclude "^6.0.0" + istanbul-reports "^3.2.0" + magicast "^0.5.2" + obug "^2.1.1" + std-env "^4.0.0-rc.1" + tinyrainbow "^3.1.0" + +"@vitest/expect@4.1.12": + version "4.1.12" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-4.1.12.tgz#e12b5f114e548252a6eee50f6619f1b8f0f89eed" + integrity sha512-vFlTt+YEu9WBpNjdGayNmlMVzrWPi1+sv853/7t2FU7hpO5gAFBKHzQmkCq5Xjpr+evegF2994LQT3hF3mRlqQ== + dependencies: + "@standard-schema/spec" "^1.1.0" + "@types/chai" "^5.2.2" + "@vitest/spy" "4.1.12" + "@vitest/utils" "4.1.12" + chai "^6.2.2" + tinyrainbow "^3.1.0" + +"@vitest/mocker@4.1.12": + version "4.1.12" + resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-4.1.12.tgz#13b241597b79695e401fd05b363bb8e79695dfac" + integrity sha512-fKShNpKZzupWZuY0z7egydvL0sqW4INEZBEExMBMAa6izlENEVHwoXgdaTCaBZ5fwlr4xf2o1wXC83n9RpO+NA== + dependencies: + "@vitest/spy" "4.1.12" + estree-walker "^3.0.3" + magic-string "^0.30.21" -"@vitest/expect@1.6.0": - version "1.6.0" - resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-1.6.0.tgz#0b3ba0914f738508464983f4d811bc122b51fb30" - integrity sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ== +"@vitest/pretty-format@4.1.12": + version "4.1.12" + resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-4.1.12.tgz#396d123c8905e0a7c3dde02d5ba84ed8f983fdbf" + integrity sha512-XW9O8+Mxx4VpCtkYdMJLZQQwsVryP9wk7cPKDxtXFfwSak6yRh5+2fep/Y0XF/ElBvJHq8bcYdi6aeuai0opZA== dependencies: - "@vitest/spy" "1.6.0" - "@vitest/utils" "1.6.0" - chai "^4.3.10" + tinyrainbow "^3.1.0" -"@vitest/runner@1.6.0": - version "1.6.0" - resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-1.6.0.tgz#a6de49a96cb33b0e3ba0d9064a3e8d6ce2f08825" - integrity sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg== +"@vitest/runner@4.1.12": + version "4.1.12" + resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-4.1.12.tgz#4c4e267b2011e7dcc5a84a9271caaaf880d80c02" + integrity sha512-NKlVKCr5QnCe8jijdMnmkpHHU+WTqiLFR6G0pHji9uRjXK9xJl1k1nTw6F3pOOj2PDcsDt2GKQMI7wIsaT491w== dependencies: - "@vitest/utils" "1.6.0" - p-limit "^5.0.0" - pathe "^1.1.1" + "@vitest/utils" "4.1.12" + pathe "^2.0.3" -"@vitest/snapshot@1.6.0": - version "1.6.0" - resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-1.6.0.tgz#deb7e4498a5299c1198136f56e6e0f692e6af470" - integrity sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ== +"@vitest/snapshot@4.1.12": + version "4.1.12" + resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-4.1.12.tgz#53670a17d3d804e651195d5babab1bf2fd83688a" + integrity sha512-ko1y1Qgb/slzQ9mr+b89Cuqud8WG1TKtsPD8r7ZUBdvVOGt85Af2HwCqHujrhwJq95hs/9RM7fcAPg57o1vlzw== dependencies: - magic-string "^0.30.5" - pathe "^1.1.1" - pretty-format "^29.7.0" + "@vitest/pretty-format" "4.1.12" + "@vitest/utils" "4.1.12" + magic-string "^0.30.21" + pathe "^2.0.3" -"@vitest/spy@1.6.0": - version "1.6.0" - resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-1.6.0.tgz#362cbd42ccdb03f1613798fde99799649516906d" - integrity sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw== - dependencies: - tinyspy "^2.2.0" +"@vitest/spy@4.1.12": + version "4.1.12" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-4.1.12.tgz#59b02f0735cdff0644e1664d433b084b4b80ec04" + integrity sha512-RGq9x4JNGNTZHEJxvN0oSiJjeSCExm0n2gZKKJZHY9cX5AVA6M7VJILBT3ZJ/tJ/0J9uWT7fETV9/btoi//vSg== -"@vitest/utils@1.6.0": - version "1.6.0" - resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-1.6.0.tgz#5c5675ca7d6f546a7b4337de9ae882e6c57896a1" - integrity sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw== +"@vitest/utils@4.1.12": + version "4.1.12" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-4.1.12.tgz#e5416ed6c2313ded30dd9eae5fc08cba11fcacb3" + integrity sha512-Q3awXrwOfEV+qc3WmO5ONyE0yO7Q1gQ9/oyHxGLZ/pl7N3NO/ZztqQwKLuwMkV+CA1JYSIeO3gkHnRDIo4AVaA== dependencies: - diff-sequences "^29.6.3" - estree-walker "^3.0.3" - loupe "^2.3.7" - pretty-format "^29.7.0" + "@vitest/pretty-format" "4.1.12" + convert-source-map "^2.0.0" + tinyrainbow "^3.1.0" -acorn-walk@^8.1.1, acorn-walk@^8.3.2: +acorn-walk@^8.1.1: version "8.3.2" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.2.tgz#7703af9415f1b6db9315d6895503862e231d34aa" integrity sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A== -acorn@^8.11.3, acorn@^8.4.1: +acorn@^8.4.1: version "8.11.3" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== @@ -565,10 +458,19 @@ array-buffer-byte-length@^1.0.0: call-bind "^1.0.5" is-array-buffer "^3.0.4" -assertion-error@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" - integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== +assertion-error@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7" + integrity sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA== + +ast-v8-to-istanbul@^1.0.0: + version "1.0.7" + resolved "https://registry.yarnpkg.com/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.7.tgz#ead45f98ca59413f048f1b1466a78b6c8a1070bb" + integrity sha512-kFL68AG6ajd8fg248zwM9GQrUWEp79gsmjum34OEXjs4yHuUMZfYKwOLW9GMmB4oNvVrj+EAGxsP7ye2UR9UlA== + dependencies: + "@jridgewell/trace-mapping" "^0.3.31" + estree-walker "^3.0.3" + js-tokens "^10.0.0" asynckit@^0.4.0: version "0.4.0" @@ -582,24 +484,6 @@ available-typed-arrays@^1.0.7: dependencies: possible-typed-array-names "^1.0.0" -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -cac@^6.7.14: - version "6.7.14" - resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.14.tgz#804e1e6f506ee363cb0e3ccbb09cad5dd9870959" - integrity sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ== - call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" @@ -611,18 +495,10 @@ call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: get-intrinsic "^1.2.4" set-function-length "^1.2.1" -chai@^4.3.10: - version "4.4.1" - resolved "https://registry.yarnpkg.com/chai/-/chai-4.4.1.tgz#3603fa6eba35425b0f2ac91a009fe924106e50d1" - integrity sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g== - dependencies: - assertion-error "^1.1.0" - check-error "^1.0.3" - deep-eql "^4.1.3" - get-func-name "^2.0.2" - loupe "^2.3.6" - pathval "^1.1.1" - type-detect "^4.0.8" +chai@^6.2.2: + version "6.3.0" + resolved "https://registry.yarnpkg.com/chai/-/chai-6.3.0.tgz#91f89eb789c2bb1ae1787d78c8518e1d3cebfc07" + integrity sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg== chalk@^2.4.2: version "2.4.2" @@ -649,13 +525,6 @@ chalk@^4.1.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -check-error@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.3.tgz#a6502e4312a7ee969f646e83bb3ddd56281bd694" - integrity sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg== - dependencies: - get-func-name "^2.0.2" - color-convert@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" @@ -687,15 +556,10 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== - -confbox@^0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.1.7.tgz#ccfc0a2bcae36a84838e83a3b7f770fb17d6c579" - integrity sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA== +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== core-js@^3.20.0, core-js@^3.26.0: version "3.37.1" @@ -707,15 +571,6 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== -cross-spawn@^7.0.3: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== - dependencies: - path-key "^3.1.0" - shebang-command "^2.0.0" - which "^2.0.1" - css.escape@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" @@ -741,7 +596,7 @@ data-urls@^5.0.0: whatwg-mimetype "^4.0.0" whatwg-url "^14.0.0" -debug@4, debug@^4.1.1, debug@^4.3.4: +debug@4, debug@^4.3.4: version "4.3.5" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== @@ -753,13 +608,6 @@ decimal.js@^10.4.3: resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA== -deep-eql@^4.1.3: - version "4.1.3" - resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-4.1.3.tgz#7c7775513092f7df98d8df9996dd085eb668cc6d" - integrity sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw== - dependencies: - type-detect "^4.0.0" - deep-equal@^2.0.5: version "2.2.3" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.2.3.tgz#af89dafb23a396c7da3e862abc0be27cf51d56e1" @@ -812,10 +660,10 @@ dequal@^2.0.3: resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== -diff-sequences@^29.6.3: - version "29.6.3" - resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" - integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== +detect-libc@^2.0.3: + version "2.1.2" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.1.2.tgz#689c5dcdc1900ef5583a4cb9f6d7b473742074ad" + integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ== diff@^4.0.1: version "4.0.2" @@ -864,34 +712,10 @@ es-get-iterator@^1.1.3: isarray "^2.0.5" stop-iteration-iterator "^1.0.0" -esbuild@^0.20.1: - version "0.20.2" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.20.2.tgz#9d6b2386561766ee6b5a55196c6d766d28c87ea1" - integrity sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g== - optionalDependencies: - "@esbuild/aix-ppc64" "0.20.2" - "@esbuild/android-arm" "0.20.2" - "@esbuild/android-arm64" "0.20.2" - "@esbuild/android-x64" "0.20.2" - "@esbuild/darwin-arm64" "0.20.2" - "@esbuild/darwin-x64" "0.20.2" - "@esbuild/freebsd-arm64" "0.20.2" - "@esbuild/freebsd-x64" "0.20.2" - "@esbuild/linux-arm" "0.20.2" - "@esbuild/linux-arm64" "0.20.2" - "@esbuild/linux-ia32" "0.20.2" - "@esbuild/linux-loong64" "0.20.2" - "@esbuild/linux-mips64el" "0.20.2" - "@esbuild/linux-ppc64" "0.20.2" - "@esbuild/linux-riscv64" "0.20.2" - "@esbuild/linux-s390x" "0.20.2" - "@esbuild/linux-x64" "0.20.2" - "@esbuild/netbsd-x64" "0.20.2" - "@esbuild/openbsd-x64" "0.20.2" - "@esbuild/sunos-x64" "0.20.2" - "@esbuild/win32-arm64" "0.20.2" - "@esbuild/win32-ia32" "0.20.2" - "@esbuild/win32-x64" "0.20.2" +es-module-lexer@^2.0.0: + version "2.3.2" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-2.3.2.tgz#311fa4f40168c1975c505477c51b23234d41ad55" + integrity sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw== escape-string-regexp@^1.0.5: version "1.0.5" @@ -905,20 +729,15 @@ estree-walker@^3.0.3: dependencies: "@types/estree" "^1.0.0" -execa@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/execa/-/execa-8.0.1.tgz#51f6a5943b580f963c3ca9c6321796db8cc39b8c" - integrity sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg== - dependencies: - cross-spawn "^7.0.3" - get-stream "^8.0.1" - human-signals "^5.0.0" - is-stream "^3.0.0" - merge-stream "^2.0.0" - npm-run-path "^5.1.0" - onetime "^6.0.0" - signal-exit "^4.1.0" - strip-final-newline "^3.0.0" +expect-type@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-1.4.0.tgz#24edf7f0cc69a44d008567ba4594ab96f3c3a3d6" + integrity sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA== + +fdir@^6.5.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350" + integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== for-each@^0.3.3: version "0.3.3" @@ -936,12 +755,7 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== - -fsevents@~2.3.2, fsevents@~2.3.3: +fsevents@~2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== @@ -956,11 +770,6 @@ functions-have-names@^1.2.3: resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== -get-func-name@^2.0.1, get-func-name@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.2.tgz#0d7cf20cd13fda808669ffa88f4ffc7a3943fc41" - integrity sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ== - get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2, get-intrinsic@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" @@ -972,23 +781,6 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2, get-intrinsic@ has-symbols "^1.0.3" hasown "^2.0.0" -get-stream@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-8.0.1.tgz#def9dfd71742cd7754a7761ed43749a27d02eca2" - integrity sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA== - -glob@^7.1.4: - version "7.2.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" - integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.1.1" - once "^1.3.0" - path-is-absolute "^1.0.0" - gopd@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" @@ -1070,11 +862,6 @@ https-proxy-agent@^7.0.4: agent-base "^7.0.2" debug "4" -human-signals@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-5.0.0.tgz#42665a284f9ae0dade3ba41ebc37eb4b852f3a28" - integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ== - iconv-lite@0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" @@ -1087,19 +874,6 @@ indent-string@^4.0.0: resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - internal-slot@^1.0.4: version "1.0.7" resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802" @@ -1189,11 +963,6 @@ is-shared-array-buffer@^1.0.2: dependencies: call-bind "^1.0.7" -is-stream@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac" - integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA== - is-string@^1.0.5, is-string@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" @@ -1226,11 +995,6 @@ isarray@^2.0.5: resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== - istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.2: version "3.2.2" resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz#2d166c4b0644d43a39f04bf6c2edd1e585f31756" @@ -1245,33 +1009,24 @@ istanbul-lib-report@^3.0.0, istanbul-lib-report@^3.0.1: make-dir "^4.0.0" supports-color "^7.1.0" -istanbul-lib-source-maps@^5.0.4: - version "5.0.4" - resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.4.tgz#1947003c72a91b6310efeb92d2a91be8804d92c2" - integrity sha512-wHOoEsNJTVltaJp8eVkm8w+GVkVNHT2YDYo53YdzQEL2gWm1hBX5cGFR9hQJtuGLebidVX7et3+dmDZrmclduw== - dependencies: - "@jridgewell/trace-mapping" "^0.3.23" - debug "^4.1.1" - istanbul-lib-coverage "^3.0.0" - -istanbul-reports@^3.1.6: - version "3.1.7" - resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.7.tgz#daed12b9e1dca518e15c056e1e537e741280fa0b" - integrity sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g== +istanbul-reports@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.2.0.tgz#cb4535162b5784aa623cee21a7252cf2c807ac93" + integrity sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA== dependencies: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" +js-tokens@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-10.0.0.tgz#dffe7599b4a8bb7fe30aff8d0235234dffb79831" + integrity sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q== + "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-tokens@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-9.0.0.tgz#0f893996d6f3ed46df7f0a3b12a03f5fd84223c1" - integrity sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ== - jsdom@^24.1.0: version "24.1.0" resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-24.1.0.tgz#0cffdabd42c506788bfecd160e8ac22d4387f971" @@ -1299,13 +1054,79 @@ jsdom@^24.1.0: ws "^8.17.0" xml-name-validator "^5.0.0" -local-pkg@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/local-pkg/-/local-pkg-0.5.0.tgz#093d25a346bae59a99f80e75f6e9d36d7e8c925c" - integrity sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg== - dependencies: - mlly "^1.4.2" - pkg-types "^1.0.3" +lightningcss-android-arm64@1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz#9a6841f88ae50fc83502903892b41af41bc2b907" + integrity sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg== + +lightningcss-darwin-arm64@1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz#c0f2c31c0bfd19fa4dd3f18e957a1f1a152097d6" + integrity sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg== + +lightningcss-darwin-x64@1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz#cb0705965acb538c6683949ce6925fb3cdf7c361" + integrity sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ== + +lightningcss-freebsd-x64@1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz#763538828b26bab2680dadafcc84ee78b0eb502b" + integrity sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg== + +lightningcss-linux-arm-gnueabihf@1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz#6862e3176a331aedbdec1ed352b4d7d0dd0784de" + integrity sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ== + +lightningcss-linux-arm64-gnu@1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz#c6a3a2ed15141daf6bdc2628930f8e39bdf473aa" + integrity sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg== + +lightningcss-linux-arm64-musl@1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz#7fa1334971fc82845f9827df6ef8a0b20914bac6" + integrity sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ== + +lightningcss-linux-x64-gnu@1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz#8b927862ea8c2bbc6831a46509244b50d9936e55" + integrity sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg== + +lightningcss-linux-x64-musl@1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz#0c525bb077dfd94404c059cfe42dad797e96aeaf" + integrity sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw== + +lightningcss-win32-arm64-msvc@1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz#850ee1103dac989cfab50e3ac22d1a69e394e63d" + integrity sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA== + +lightningcss-win32-x64-msvc@1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz#e343ae152eed3609dc6e11949d1a3bf39a1c946f" + integrity sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA== + +lightningcss@^1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/lightningcss/-/lightningcss-1.33.0.tgz#c08867d71a79385c6e190214fd72fef3e5f95f0b" + integrity sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA== + dependencies: + detect-libc "^2.0.3" + optionalDependencies: + lightningcss-android-arm64 "1.33.0" + lightningcss-darwin-arm64 "1.33.0" + lightningcss-darwin-x64 "1.33.0" + lightningcss-freebsd-x64 "1.33.0" + lightningcss-linux-arm-gnueabihf "1.33.0" + lightningcss-linux-arm64-gnu "1.33.0" + lightningcss-linux-arm64-musl "1.33.0" + lightningcss-linux-x64-gnu "1.33.0" + lightningcss-linux-x64-musl "1.33.0" + lightningcss-win32-arm64-msvc "1.33.0" + lightningcss-win32-x64-msvc "1.33.0" lodash@^4.17.21: version "4.17.21" @@ -1319,33 +1140,26 @@ loose-envify@^1.1.0: dependencies: js-tokens "^3.0.0 || ^4.0.0" -loupe@^2.3.6, loupe@^2.3.7: - version "2.3.7" - resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.7.tgz#6e69b7d4db7d3ab436328013d37d1c8c3540c697" - integrity sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA== - dependencies: - get-func-name "^2.0.1" - lz-string@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ== -magic-string@^0.30.5: - version "0.30.10" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.10.tgz#123d9c41a0cb5640c892b041d4cfb3bd0aa4b39e" - integrity sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ== +magic-string@^0.30.21: + version "0.30.21" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.21.tgz#56763ec09a0fa8091df27879fd94d19078c00d91" + integrity sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ== dependencies: - "@jridgewell/sourcemap-codec" "^1.4.15" + "@jridgewell/sourcemap-codec" "^1.5.5" -magicast@^0.3.3: - version "0.3.4" - resolved "https://registry.yarnpkg.com/magicast/-/magicast-0.3.4.tgz#bbda1791d03190a24b00ff3dd18151e7fd381d19" - integrity sha512-TyDF/Pn36bBji9rWKHlZe+PZb6Mx5V8IHCSxk7X4aljM4e/vyDvZZYwHewdVaqiA0nb3ghfHU/6AUpDxWoER2Q== +magicast@^0.5.2: + version "0.5.5" + resolved "https://registry.yarnpkg.com/magicast/-/magicast-0.5.5.tgz#66dfd87ba2a40b568d97ff0d2b888ad3580519cb" + integrity sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA== dependencies: - "@babel/parser" "^7.24.4" - "@babel/types" "^7.24.0" - source-map-js "^1.2.0" + "@babel/parser" "^7.29.7" + "@babel/types" "^7.29.7" + source-map-js "^1.2.1" make-dir@^4.0.0: version "4.0.0" @@ -1359,11 +1173,6 @@ make-error@^1.1.1: resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== -merge-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" - integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== - mime-db@1.52.0: version "1.52.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" @@ -1376,42 +1185,20 @@ mime-types@^2.1.12: dependencies: mime-db "1.52.0" -mimic-fn@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc" - integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== - min-indent@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== -minimatch@^3.0.4, minimatch@^3.1.1: - version "3.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - -mlly@^1.4.2, mlly@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.7.0.tgz#587383ae40dda23cadb11c3c3cc972b277724271" - integrity sha512-U9SDaXGEREBYQgfejV97coK0UL1r+qnF2SyO9A3qcI8MzKnsIFKHNVEkrDyNncQTKQQumsasmeq84eNMdBfsNQ== - dependencies: - acorn "^8.11.3" - pathe "^1.1.2" - pkg-types "^1.1.0" - ufo "^1.5.3" - ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -nanoid@^3.3.7: - version "3.3.7" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" - integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== +nanoid@^3.3.20: + version "3.3.20" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.20.tgz#dfd4444d85aa10111ef66ba85ed72e08ba0855d8" + integrity sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ== node-fetch@^2.6.7: version "2.7.0" @@ -1420,13 +1207,6 @@ node-fetch@^2.6.7: dependencies: whatwg-url "^5.0.0" -npm-run-path@^5.1.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-5.3.0.tgz#e23353d0ebb9317f174e93417e4a4d82d0249e9f" - integrity sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ== - dependencies: - path-key "^4.0.0" - nwsapi@^2.2.10: version "2.2.10" resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.10.tgz#0b77a68e21a0b483db70b11fad055906e867cda8" @@ -1460,26 +1240,10 @@ object.assign@^4.1.4: has-symbols "^1.0.3" object-keys "^1.1.1" -once@^1.3.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== - dependencies: - wrappy "1" - -onetime@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-6.0.0.tgz#7c24c18ed1fd2e9bca4bd26806a33613c77d34b4" - integrity sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ== - dependencies: - mimic-fn "^4.0.0" - -p-limit@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-5.0.0.tgz#6946d5b7140b649b7a33a027d89b4c625b3a5985" - integrity sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ== - dependencies: - yocto-queue "^1.0.0" +obug@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/obug/-/obug-2.2.1.tgz#9c453efeeda822050eb9774242e969c784d64d78" + integrity sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q== parse5@^7.1.2: version "7.1.2" @@ -1488,58 +1252,39 @@ parse5@^7.1.2: dependencies: entities "^4.4.0" -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== - -path-key@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" - integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== - -path-key@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-4.0.0.tgz#295588dc3aee64154f877adb9d780b81c554bf18" - integrity sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ== - -pathe@^1.1.1, pathe@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.2.tgz#6c4cb47a945692e48a1ddd6e4094d170516437ec" - integrity sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ== - -pathval@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" - integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== +pathe@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716" + integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w== picocolors@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== -pkg-types@^1.0.3, pkg-types@^1.1.0: +picocolors@^1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.1.1.tgz#07b626880749beb607b0c817af63aac1845a73f2" - integrity sha512-ko14TjmDuQJ14zsotODv7dBlwxKhUKQEhuhmbqo1uCi9BB0Z2alo/wAXg6q1dTR5TyuqYyWhjtfe/Tsh+X28jQ== - dependencies: - confbox "^0.1.7" - mlly "^1.7.0" - pathe "^1.1.2" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +picomatch@^4.0.3, picomatch@^4.0.4, picomatch@^4.0.7: + version "4.0.7" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.7.tgz#6313360034ccb36b3dc61ecbdff78121f90fe21f" + integrity sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA== possible-typed-array-names@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== -postcss@^8.4.38: - version "8.4.38" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e" - integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A== +postcss@^8.5.29: + version "8.5.31" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.31.tgz#d44731fa02f8ae8371629ec7220e2ae0dbdf23ec" + integrity sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg== dependencies: - nanoid "^3.3.7" - picocolors "^1.0.0" - source-map-js "^1.2.0" + nanoid "^3.3.20" + picocolors "^1.1.1" + source-map-js "^1.2.2" prettier@3.2.5: version "3.2.5" @@ -1555,15 +1300,6 @@ pretty-format@^27.0.2: ansi-styles "^5.0.0" react-is "^17.0.1" -pretty-format@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812" - integrity sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ== - dependencies: - "@jest/schemas" "^29.6.3" - ansi-styles "^5.0.0" - react-is "^18.0.0" - psl@^1.1.33: version "1.9.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" @@ -1592,11 +1328,6 @@ react-is@^17.0.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== -react-is@^18.0.0: - version "18.3.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" - integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== - react@^18.2.0: version "18.3.1" resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" @@ -1637,30 +1368,29 @@ rfdc@^1.3.0: resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.1.tgz#2b6d4df52dffe8bb346992a10ea9451f24373a8f" integrity sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg== -rollup@^4.13.0: - version "4.18.0" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.18.0.tgz#497f60f0c5308e4602cf41136339fbf87d5f5dda" - integrity sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg== +rolldown@~1.2.12: + version "1.2.14" + resolved "https://registry.yarnpkg.com/rolldown/-/rolldown-1.2.14.tgz#4005c51ae81e1d07b2e0d22f56d1f040f85aac96" + integrity sha512-eTBTTqMR6uHKLgcUqpJP8ntzDI6V4LYA8negAMBEI7gkgVvOdrx6VsvS2Ym+83KhOPdLCHWJu0w7UQFJZhHKNw== dependencies: - "@types/estree" "1.0.5" + "@oxc-project/types" "=0.154.0" + "@rolldown/pluginutils" "^1.0.0" optionalDependencies: - "@rollup/rollup-android-arm-eabi" "4.18.0" - "@rollup/rollup-android-arm64" "4.18.0" - "@rollup/rollup-darwin-arm64" "4.18.0" - "@rollup/rollup-darwin-x64" "4.18.0" - "@rollup/rollup-linux-arm-gnueabihf" "4.18.0" - "@rollup/rollup-linux-arm-musleabihf" "4.18.0" - "@rollup/rollup-linux-arm64-gnu" "4.18.0" - "@rollup/rollup-linux-arm64-musl" "4.18.0" - "@rollup/rollup-linux-powerpc64le-gnu" "4.18.0" - "@rollup/rollup-linux-riscv64-gnu" "4.18.0" - "@rollup/rollup-linux-s390x-gnu" "4.18.0" - "@rollup/rollup-linux-x64-gnu" "4.18.0" - "@rollup/rollup-linux-x64-musl" "4.18.0" - "@rollup/rollup-win32-arm64-msvc" "4.18.0" - "@rollup/rollup-win32-ia32-msvc" "4.18.0" - "@rollup/rollup-win32-x64-msvc" "4.18.0" - fsevents "~2.3.2" + "@rolldown/binding-android-arm-eabi" "1.2.14" + "@rolldown/binding-android-arm64" "1.2.14" + "@rolldown/binding-darwin-arm64" "1.2.14" + "@rolldown/binding-darwin-x64" "1.2.14" + "@rolldown/binding-freebsd-x64" "1.2.14" + "@rolldown/binding-linux-arm-gnueabihf" "1.2.14" + "@rolldown/binding-linux-arm64-gnu" "1.2.14" + "@rolldown/binding-linux-arm64-musl" "1.2.14" + "@rolldown/binding-linux-ppc64-gnu" "1.2.14" + "@rolldown/binding-linux-s390x-gnu" "1.2.14" + "@rolldown/binding-linux-x64-gnu" "1.2.14" + "@rolldown/binding-linux-x64-musl" "1.2.14" + "@rolldown/binding-openharmony-arm64" "1.2.14" + "@rolldown/binding-win32-arm64-msvc" "1.2.14" + "@rolldown/binding-win32-x64-msvc" "1.2.14" rrweb-cssom@^0.6.0: version "0.6.0" @@ -1718,18 +1448,6 @@ set-function-name@^2.0.1: functions-have-names "^1.2.3" has-property-descriptors "^1.0.2" -shebang-command@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" - integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== - dependencies: - shebang-regex "^3.0.0" - -shebang-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" - integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== - side-channel@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" @@ -1745,25 +1463,20 @@ siginfo@^2.0.0: resolved "https://registry.yarnpkg.com/siginfo/-/siginfo-2.0.0.tgz#32e76c70b79724e3bb567cb9d543eb858ccfaf30" integrity sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g== -signal-exit@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" - integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== - -source-map-js@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" - integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== +source-map-js@^1.2.1, source-map-js@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.2.tgz#79fb88d97535a21cb27162cb05d211b67acea4ab" + integrity sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw== stackback@0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b" integrity sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw== -std-env@^3.5.0: - version "3.7.0" - resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.7.0.tgz#c9f7386ced6ecf13360b6c6c55b8aaa4ef7481d2" - integrity sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg== +std-env@^4.0.0-rc.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/std-env/-/std-env-4.3.0.tgz#4e7231b61fd83219aa7f76f71fa8818b07e41321" + integrity sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag== stop-iteration-iterator@^1.0.0: version "1.0.0" @@ -1772,11 +1485,6 @@ stop-iteration-iterator@^1.0.0: dependencies: internal-slot "^1.0.4" -strip-final-newline@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd" - integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw== - strip-indent@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" @@ -1784,13 +1492,6 @@ strip-indent@^3.0.0: dependencies: min-indent "^1.0.0" -strip-literal@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/strip-literal/-/strip-literal-2.1.0.tgz#6d82ade5e2e74f5c7e8739b6c84692bd65f0bd2a" - integrity sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw== - dependencies: - js-tokens "^9.0.0" - supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -1810,34 +1511,28 @@ symbol-tree@^3.2.4: resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== -test-exclude@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" - integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== - dependencies: - "@istanbuljs/schema" "^0.1.2" - glob "^7.1.4" - minimatch "^3.0.4" - -tinybench@^2.5.1: - version "2.8.0" - resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.8.0.tgz#30e19ae3a27508ee18273ffed9ac7018949acd7b" - integrity sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw== +tinybench@^2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.9.0.tgz#103c9f8ba6d7237a47ab6dd1dcff77251863426b" + integrity sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg== -tinypool@^0.8.3: - version "0.8.4" - resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-0.8.4.tgz#e217fe1270d941b39e98c625dcecebb1408c9aa8" - integrity sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ== +tinyexec@^1.0.2: + version "1.3.1" + resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-1.3.1.tgz#16a2e3c6e23fafce72640e678e651db36145b9c6" + integrity sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA== -tinyspy@^2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-2.2.1.tgz#117b2342f1f38a0dbdcc73a50a454883adf861d1" - integrity sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A== +tinyglobby@^0.2.15, tinyglobby@^0.2.17: + version "0.2.17" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.17.tgz#562a9a6c9eb2b3b123d39719f9af5bb44fcd7631" + integrity sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g== + dependencies: + fdir "^6.5.0" + picomatch "^4.0.4" -to-fast-properties@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" - integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== +tinyrainbow@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/tinyrainbow/-/tinyrainbow-3.2.0.tgz#f62df4fda24415b353c08b9c38934b4b13877655" + integrity sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A== tough-cookie@^4.1.4: version "4.1.4" @@ -1880,21 +1575,11 @@ ts-node@^10.9.1: v8-compile-cache-lib "^3.0.1" yn "3.1.1" -type-detect@^4.0.0, type-detect@^4.0.8: - version "4.0.8" - resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" - integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== - typescript@^4.8.3: version "4.9.5" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== -ufo@^1.5.3: - version "1.5.3" - resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.5.3.tgz#3325bd3c977b6c6cd3160bf4ff52989adc9d3344" - integrity sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw== - universalify@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" @@ -1913,53 +1598,44 @@ v8-compile-cache-lib@^3.0.1: resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== -vite-node@1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-1.6.0.tgz#2c7e61129bfecc759478fa592754fd9704aaba7f" - integrity sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw== - dependencies: - cac "^6.7.14" - debug "^4.3.4" - pathe "^1.1.1" - picocolors "^1.0.0" - vite "^5.0.0" - -vite@^5.0.0: - version "5.2.12" - resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.12.tgz#3536c93c58ba18edea4915a2ac573e6537409d97" - integrity sha512-/gC8GxzxMK5ntBwb48pR32GGhENnjtY30G4A0jemunsBkiEZFw60s8InGpN8gkhHEkjnRK1aSAxeQgwvFhUHAA== +"vite@^6.0.0 || ^7.0.0 || ^8.0.0": + version "8.3.4" + resolved "https://registry.yarnpkg.com/vite/-/vite-8.3.4.tgz#ba08678ba41ba53f41c251b1772946c27b1c874e" + integrity sha512-IftLE8phJhdNRIGNEV4JCCvfNScoZQVfcXaN8TQropWGJtOHcu+iAB9pHcuGvmEb/kddw0LBe/jS8ztdC6xdiQ== dependencies: - esbuild "^0.20.1" - postcss "^8.4.38" - rollup "^4.13.0" + lightningcss "^1.33.0" + picomatch "^4.0.7" + postcss "^8.5.29" + rolldown "~1.2.12" + tinyglobby "^0.2.17" optionalDependencies: fsevents "~2.3.3" -vitest@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/vitest/-/vitest-1.6.0.tgz#9d5ad4752a3c451be919e412c597126cffb9892f" - integrity sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA== - dependencies: - "@vitest/expect" "1.6.0" - "@vitest/runner" "1.6.0" - "@vitest/snapshot" "1.6.0" - "@vitest/spy" "1.6.0" - "@vitest/utils" "1.6.0" - acorn-walk "^8.3.2" - chai "^4.3.10" - debug "^4.3.4" - execa "^8.0.1" - local-pkg "^0.5.0" - magic-string "^0.30.5" - pathe "^1.1.1" - picocolors "^1.0.0" - std-env "^3.5.0" - strip-literal "^2.0.0" - tinybench "^2.5.1" - tinypool "^0.8.3" - vite "^5.0.0" - vite-node "1.6.0" - why-is-node-running "^2.2.2" +vitest@^4.0.18: + version "4.1.12" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-4.1.12.tgz#b372587e9c000f165f1757793178ea6b5631f82e" + integrity sha512-5eWdz2yb3ajxWF3C8C4TjkXl7FGm3n2E/VlazALrQHzK7nYMSGFnCZto0bbPNl77AwkOZVLk0YnBAejUu0NiWA== + dependencies: + "@vitest/expect" "4.1.12" + "@vitest/mocker" "4.1.12" + "@vitest/pretty-format" "4.1.12" + "@vitest/runner" "4.1.12" + "@vitest/snapshot" "4.1.12" + "@vitest/spy" "4.1.12" + "@vitest/utils" "4.1.12" + es-module-lexer "^2.0.0" + expect-type "^1.3.0" + magic-string "^0.30.21" + obug "^2.1.1" + pathe "^2.0.3" + picomatch "^4.0.3" + std-env "^4.0.0-rc.1" + tinybench "^2.9.0" + tinyexec "^1.0.2" + tinyglobby "^0.2.15" + tinyrainbow "^3.1.0" + vite "^6.0.0 || ^7.0.0 || ^8.0.0" + why-is-node-running "^2.3.0" w3c-xmlserializer@^5.0.0: version "5.0.0" @@ -2038,26 +1714,14 @@ which-typed-array@^1.1.13: gopd "^1.0.1" has-tostringtag "^1.0.2" -which@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" - integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== - dependencies: - isexe "^2.0.0" - -why-is-node-running@^2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/why-is-node-running/-/why-is-node-running-2.2.2.tgz#4185b2b4699117819e7154594271e7e344c9973e" - integrity sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA== +why-is-node-running@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz#a3f69a97107f494b3cdc3bdddd883a7d65cebf04" + integrity sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w== dependencies: siginfo "^2.0.0" stackback "0.0.2" -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== - ws@^8.17.0: version "8.17.0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.0.tgz#d145d18eca2ed25aaf791a183903f7be5e295fea" @@ -2077,8 +1741,3 @@ yn@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== - -yocto-queue@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251" - integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==