diff --git a/.github/workflows/main-pipeline.yaml b/.github/workflows/main-pipeline.yaml index 12115809..f0d644e9 100644 --- a/.github/workflows/main-pipeline.yaml +++ b/.github/workflows/main-pipeline.yaml @@ -20,6 +20,7 @@ jobs: e2e_node_ssr_only: ${{ steps.filter.outputs.e2e_node_ssr_only }} e2e_node_ssr_web_vanilla: ${{ steps.filter.outputs.e2e_node_ssr_web_vanilla }} e2e_web: ${{ steps.filter.outputs.e2e_web }} + e2e_web_react: ${{ steps.filter.outputs.e2e_web_react }} e2e_react_native_android: ${{ steps.filter.outputs.e2e_react_native_android }} steps: - uses: namespacelabs/nscloud-checkout-action@v8 @@ -70,6 +71,14 @@ jobs: - 'package.json' - 'pnpm-lock.yaml' - '.github/workflows/main-pipeline.yaml' + e2e_web_react: + - 'implementations/web-react/**' + - 'lib/**' + - 'platforms/**' + - 'universal/**' + - 'package.json' + - 'pnpm-lock.yaml' + - '.github/workflows/main-pipeline.yaml' e2e_react_native_android: - 'implementations/react-native/**' - 'platforms/javascript/react-native/**' @@ -468,6 +477,108 @@ jobs: ./implementations/web-vanilla/test-results/ retention-days: 1 + e2e-web-react: + name: 🖥️ E2E Web React + runs-on: namespace-profile-linux-8-vcpu-16-gb-ram-optimal + timeout-minutes: 15 + needs: [setup, changes, build] + if: needs.changes.outputs.e2e_web_react == 'true' + steps: + - uses: namespacelabs/nscloud-checkout-action@v8 + + - name: Create .env and copy to implementations + run: | + cat > .env << 'EOF' + DOTENV_CONFIG_QUIET=true + PUBLIC_NINETAILED_CLIENT_ID=${{secrets.NINETAILED_CLIENT_ID}} + PUBLIC_NINETAILED_ENVIRONMENT=${{secrets.NINETAILED_ENVIRONMENT}} + PUBLIC_EXPERIENCE_API_BASE_URL=http://localhost:8000/experience/ + PUBLIC_INSIGHTS_API_BASE_URL=http://localhost:8000/insights/ + PUBLIC_CONTENTFUL_TOKEN=${{secrets.CONTENTFUL_TOKEN}} + PUBLIC_CONTENTFUL_PREVIEW_TOKEN=${{secrets.CONTENTFUL_PREVIEW_TOKEN}} + PUBLIC_CONTENTFUL_ENVIRONMENT=${{secrets.CONTENTFUL_ENVIRONMENT}} + PUBLIC_CONTENTFUL_SPACE_ID=${{secrets.CONTENTFUL_SPACE_ID}} + PUBLIC_CONTENTFUL_CDA_HOST=localhost:8000 + PUBLIC_CONTENTFUL_BASE_PATH=contentful + EOF + cp .env implementations/node-ssr-only/ + cp .env implementations/web-react/ + + - uses: actions/setup-node@v6 + with: + node-version-file: '.nvmrc' + package-manager-cache: false + + - uses: pnpm/action-setup@v4 + + - name: Set up caches (Namespace) + uses: namespacelabs/nscloud-cache-action@v1 + with: + cache: | + pnpm + playwright + apt + + - run: pnpm install --prefer-offline --frozen-lockfile + - uses: actions/download-artifact@v6 + with: + name: sdk-package-tarballs + path: pkgs + - run: pnpm store prune + - run: pnpm run implementation:web-react -- implementation:install -- --no-frozen-lockfile + - run: pnpm run implementation:web-react -- implementation:playwright:install -- --with-deps + - name: Start Web React app and mocks + run: | + pnpm run implementation:web-react -- serve:mocks > /tmp/web-react-mocks.log 2>&1 + pnpm run implementation:web-react -- build > /tmp/web-react-build.log 2>&1 + pnpm --dir implementations/web-react exec rsbuild preview --host localhost --port 3001 > /tmp/web-react-app.log 2>&1 & + echo $! > /tmp/web-react-app.pid + + for i in $(seq 1 90); do + # Use a plain HTTP probe (no --fail): we only need to know the server is listening. + if curl -sS http://localhost:3001 >/dev/null; then + echo "Web React app is ready" + exit 0 + fi + echo "Waiting for Web React app... ($i/90)" + sleep 1 + done + + echo "Web React app failed to start in time" + echo "=== web-react build logs ===" + cat /tmp/web-react-build.log || true + echo "=== web-react app logs ===" + cat /tmp/web-react-app.log || true + echo "=== web-react mocks logs ===" + cat /tmp/web-react-mocks.log || true + exit 1 + - run: pnpm run implementation:web-react -- implementation:test:e2e:run + - name: Dump Web React logs on failure + if: ${{ failure() }} + run: | + echo "=== web-react build logs ===" + cat /tmp/web-react-build.log || true + echo "=== web-react app logs ===" + cat /tmp/web-react-app.log || true + echo "=== web-react mocks logs ===" + cat /tmp/web-react-mocks.log || true + echo "=== pm2 list ===" + pm2 list || true + - name: Stop Web React app and mocks + if: ${{ always() }} + run: | + kill "$(cat /tmp/web-react-app.pid)" 2>/dev/null || true + pnpm run implementation:web-react -- serve:stop || true + + - uses: actions/upload-artifact@v6 + if: ${{ !cancelled() }} + with: + name: ci-results-web-react + path: | + ./implementations/web-react/playwright-report/ + ./implementations/web-react/test-results/ + retention-days: 1 + e2e-react-native-android: name: 📱 E2E React Native Android runs-on: namespace-profile-linux-16-vcpu-32-gb-ram-optimal diff --git a/implementations/web-react/.env.example b/implementations/web-react/.env.example new file mode 100644 index 00000000..75fcde25 --- /dev/null +++ b/implementations/web-react/.env.example @@ -0,0 +1,15 @@ +DOTENV_CONFIG_QUIET=true + +PUBLIC_NINETAILED_CLIENT_ID="mock-client-id" +PUBLIC_NINETAILED_ENVIRONMENT="main" + +PUBLIC_EXPERIENCE_API_BASE_URL="http://localhost:8000/experience/" +PUBLIC_INSIGHTS_API_BASE_URL="http://localhost:8000/insights/" + +PUBLIC_CONTENTFUL_TOKEN="mock-token" +PUBLIC_CONTENTFUL_PREVIEW_TOKEN="mock-preview-token" +PUBLIC_CONTENTFUL_ENVIRONMENT="master" +PUBLIC_CONTENTFUL_SPACE_ID="mock-space-id" + +PUBLIC_CONTENTFUL_CDA_HOST="localhost:8000" +PUBLIC_CONTENTFUL_BASE_PATH="contentful" diff --git a/implementations/web-react/.npmrc b/implementations/web-react/.npmrc new file mode 100644 index 00000000..135f7a0d --- /dev/null +++ b/implementations/web-react/.npmrc @@ -0,0 +1 @@ +shared-workspace-lockfile=false diff --git a/implementations/web-react/README.md b/implementations/web-react/README.md new file mode 100644 index 00000000..2b6a5557 --- /dev/null +++ b/implementations/web-react/README.md @@ -0,0 +1,174 @@ +# Web React Reference Implementation + +Reference implementation demonstrating `@contentful/optimization-web` usage in a React web +application. + +> **Note:** This implementation uses [Rsbuild](https://rsbuild.dev/) for consistency with the SDK +> build tooling. If you're creating your own React application, you can use any build tool you +> prefer (Vite, Create React App, Next.js, etc.) — the SDK integration patterns demonstrated here +> will work the same way. + +## Overview + +This implementation provides a thin React adapter layer over `@contentful/optimization-web`, +demonstrating: + +- `OptimizationProvider` context for SDK state management +- React hooks for SDK state subscriptions +- Personalization resolution and variant rendering +- Rich Text rendering via `@contentful/rich-text-react-renderer` +- Analytics event tracking +- Live updates behavior +- SPA navigation tracking with React Router v7 +- Offline queue/recovery handling + +## Prerequisites + +- Node.js >= 16.20.0 +- pnpm 10.x + +## Setup + +From the **repository root**: + +```bash +# Build SDK packages (required for local development) +pnpm build:pkgs + +# Install implementation dependencies +pnpm run implementation:run -- web-react implementation:install +``` + +## Development + +From the **repository root**: + +```bash +# Start development server +pnpm run implementation:web-react dev + +# Build for production +pnpm run implementation:web-react build + +# Preview production build +pnpm run implementation:web-react preview + +# Type checking +pnpm run implementation:web-react typecheck +``` + +Or from the **implementation directory** (`implementations/web-react`): + +```bash +pnpm dev +pnpm build +pnpm preview +pnpm typecheck +``` + +## Testing + +### E2E Tests + +```bash +# In terminal 1: start mocks + app preview +pnpm run implementation:web-react serve + +# In terminal 2: run Playwright tests +pnpm run implementation:web-react test:e2e + +# Interactive Playwright UI +pnpm run implementation:web-react test:e2e:ui + +# Generate tests with Playwright codegen +pnpm run implementation:web-react test:e2e:codegen +``` + +## Environment Variables + +Copy `.env.example` to `.env` and configure: + +```bash +cp .env.example .env +``` + +See `.env.example` for available configuration options. The implementation reads from +`import.meta.env` directly and falls back to local mock-safe defaults, so it can run without extra +env wiring. To use local mock Contentful endpoints, set `PUBLIC_CONTENTFUL_CDA_HOST=localhost:8000` +and `PUBLIC_CONTENTFUL_BASE_PATH=contentful`. + +## Project Structure + +``` +web-react/ +├── src/ +│ ├── main.tsx # Application entry point +│ ├── App.tsx # Root component +│ ├── optimization/ # SDK React adapter +│ │ ├── OptimizationProvider.tsx +│ │ ├── hooks/ +│ │ └── components/ +│ ├── pages/ # Route pages +│ └── components/ # UI components +├── e2e/ # Playwright E2E tests +├── public/ # Static assets +├── index.html # HTML template +├── rsbuild.config.ts # Rsbuild configuration +├── tsconfig.json # TypeScript configuration +└── package.json +``` + +## SDK Integration Patterns + +This implementation demonstrates how to build a React adapter for `@contentful/optimization-web`. +Key patterns include: + +### Provider Setup + +```tsx +import { OptimizationProvider } from './optimization' + +function App() { + return ( + + + + ) +} +``` + +### Using Hooks + +```tsx +import { usePersonalization, useOptimization } from './optimization' + +function MyComponent() { + const { sdk, isReady } = useOptimization() + const { resolveEntry } = usePersonalization() + const resolved = resolveEntry(baseEntry) + + // ... +} +``` + +### Analytics Tracking + +```tsx +import { useAnalytics } from './optimization' + +function TrackedComponent() { + const { trackView } = useAnalytics() + + useEffect(() => { + void trackView({ componentId: 'component-id' }) + }, []) + + // ... +} +``` + +## Related + +- [React Native Implementation](../react-native/) - Reference implementation for React Native +- [Web Vanilla Implementation](../web-vanilla/) - Reference implementation for vanilla JavaScript +- [@contentful/optimization-web](../../../platforms/javascript/web/) - Web SDK package diff --git a/implementations/web-react/e2e/displays-identified-user-variants.spec.ts b/implementations/web-react/e2e/displays-identified-user-variants.spec.ts new file mode 100644 index 00000000..70c264d1 --- /dev/null +++ b/implementations/web-react/e2e/displays-identified-user-variants.spec.ts @@ -0,0 +1,42 @@ +import { expect, test } from '@playwright/test' + +test.describe('identified user', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/') + await page.waitForLoadState('domcontentloaded') + await expect(page.getByRole('heading', { name: 'Utilities' })).toBeVisible() + + await page.getByRole('button', { name: 'Identify' }).click() + await expect(page.getByRole('button', { name: 'Reset Profile' })).toBeVisible() + + await page.reload() + await page.waitForLoadState('domcontentloaded') + await expect(page.getByRole('heading', { name: 'Utilities' })).toBeVisible() + await expect(page.getByRole('button', { name: 'Reset Profile' })).toBeVisible() + }) + + test('renders identified variants', async ({ page }) => { + await expect( + page.getByText('This is a variant content entry for identified users.'), + ).toBeVisible() + await expect(page.getByText('This is a level 0 nested variant entry.')).toBeVisible() + }) + + test('reset persists unidentified state across reload', async ({ page }) => { + await page.getByRole('button', { name: 'Reset Profile' }).click() + await expect(page.getByRole('button', { name: 'Identify' })).toBeVisible() + + await page.reload() + await page.waitForLoadState('domcontentloaded') + await expect(page.getByRole('heading', { name: 'Utilities' })).toBeVisible() + + await expect(page.getByRole('button', { name: 'Identify' })).toBeVisible() + await expect( + page.getByText('This is a baseline content entry for all identified or unidentified users.'), + ).toBeVisible() + await expect(page.getByText('This is a level 0 nested baseline entry.')).toBeVisible() + await expect( + page.getByText('This is a variant content entry for identified users.'), + ).toHaveCount(0) + }) +}) diff --git a/implementations/web-react/e2e/displays-unidentified-user-variants.spec.ts b/implementations/web-react/e2e/displays-unidentified-user-variants.spec.ts new file mode 100644 index 00000000..463a964b --- /dev/null +++ b/implementations/web-react/e2e/displays-unidentified-user-variants.spec.ts @@ -0,0 +1,24 @@ +import { expect, test } from '@playwright/test' + +test.describe('unidentified user', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/') + await page.waitForLoadState('domcontentloaded') + await expect(page.getByRole('heading', { name: 'Utilities' })).toBeVisible() + }) + + test('renders utility panel and entries', async ({ page }) => { + await expect(page.getByRole('heading', { name: 'Utilities' })).toBeVisible() + await expect(page.getByRole('heading', { name: 'Auto Observed Entries' })).toBeVisible() + await expect(page.getByRole('heading', { name: 'Manually Observed Entries' })).toBeVisible() + + await expect( + page.getByText( + 'This is a merge tag content entry that displays the visitor\'s continent "EU" embedded within the text.', + ), + ).toBeVisible() + + await expect(page.getByText('This is a level 0 nested baseline entry.')).toBeVisible() + await expect(page.getByText('This is a variant content entry for new visitors.')).toBeVisible() + }) +}) diff --git a/implementations/web-react/e2e/events-consent-gating.spec.ts b/implementations/web-react/e2e/events-consent-gating.spec.ts new file mode 100644 index 00000000..90b11959 --- /dev/null +++ b/implementations/web-react/e2e/events-consent-gating.spec.ts @@ -0,0 +1,40 @@ +import { type Page, expect, test } from '@playwright/test' + +async function scrollThroughEntries(page: Page): Promise { + const entries = page.locator('[data-testid^="content-"]') + const entryCount = await entries.count() + + for (let index = 0; index < entryCount; index += 1) { + await entries.nth(index).scrollIntoViewIfNeeded() + } +} + +test.describe('consent gating', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/') + await page.waitForLoadState('domcontentloaded') + await expect(page.getByRole('heading', { name: 'Utilities' })).toBeVisible() + }) + + test('allows page events without consent but gates component view events', async ({ page }) => { + const pageEvents = page.locator('[data-testid^="event-page-"]') + const componentEvents = page.locator('li').filter({ hasText: 'Component:' }) + + await expect(pageEvents.first()).toBeVisible() + + await scrollThroughEntries(page) + await expect(componentEvents).toHaveCount(0) + }) + + test('emits component view events after consent is accepted', async ({ page }) => { + const pageEvents = page.locator('[data-testid^="event-page-"]') + const componentEvents = page.locator('li').filter({ hasText: 'Component:' }) + + await expect(pageEvents.first()).toBeVisible() + + await page.getByRole('button', { name: 'Accept Consent' }).click() + await scrollThroughEntries(page) + + await expect.poll(async () => await componentEvents.count()).toBeGreaterThan(0) + }) +}) diff --git a/implementations/web-react/e2e/navigation-page-events.spec.ts b/implementations/web-react/e2e/navigation-page-events.spec.ts new file mode 100644 index 00000000..5f192762 --- /dev/null +++ b/implementations/web-react/e2e/navigation-page-events.spec.ts @@ -0,0 +1,31 @@ +import { expect, test } from '@playwright/test' + +test.describe('navigation page events', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/') + await page.waitForLoadState('domcontentloaded') + await expect(page.getByRole('heading', { name: 'Utilities' })).toBeVisible() + }) + + test('emits page events on route navigation', async ({ page }) => { + const pageEventLocator = page.locator('[data-testid^="event-page-"]') + await expect(pageEventLocator.first()).toBeVisible() + const initialPageEventCount = await pageEventLocator.count() + + await page.getByTestId('link-page-two').click() + await expect(page).toHaveURL(/\/page-two$/) + await expect(page.getByTestId('page-two-view')).toBeVisible() + + await expect + .poll(async () => await pageEventLocator.count()) + .toBeGreaterThan(initialPageEventCount) + + const afterPageTwoCount = await pageEventLocator.count() + + await page.getByTestId('link-back-home').click() + await expect(page).toHaveURL(/\/$/) + await expect(page.getByRole('heading', { name: 'Utilities' })).toBeVisible() + + await expect.poll(async () => await pageEventLocator.count()).toBeGreaterThan(afterPageTwoCount) + }) +}) diff --git a/implementations/web-react/index.html b/implementations/web-react/index.html new file mode 100644 index 00000000..d66f31c2 --- /dev/null +++ b/implementations/web-react/index.html @@ -0,0 +1,11 @@ + + + + + + Optimization Web React Reference + + +
+ + diff --git a/implementations/web-react/package.json b/implementations/web-react/package.json new file mode 100644 index 00000000..d49b0951 --- /dev/null +++ b/implementations/web-react/package.json @@ -0,0 +1,55 @@ +{ + "name": "@implementation/web-react", + "private": true, + "version": "0.0.0", + "description": "Reference implementation for React Web applications", + "license": "MIT", + "type": "module", + "scripts": { + "dev": "rsbuild dev", + "build": "rsbuild build", + "clean": "rimraf ./dist ./coverage ./playwright-report ./test-results .tsbuildinfo", + "preview": "rsbuild preview", + "serve": "pnpm serve:mocks && pnpm serve:app", + "serve:app": "pnpm build && rsbuild preview --host localhost --port 3001", + "serve:app:stop": "echo 'Rsbuild preview stops automatically'", + "serve:mocks": "pm2 start --name web-react-mocks \"pnpm --dir ../../lib/mocks serve\"", + "serve:mocks:stop": "pm2 stop web-react-mocks && pm2 delete web-react-mocks", + "serve:stop": "pnpm serve:mocks:stop", + "test:e2e": "playwright test", + "test:e2e:codegen": "playwright codegen", + "test:e2e:report": "playwright show-report", + "test:e2e:ui": "playwright test --ui", + "test:unit": "echo \"No unit tests necessary\"", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@contentful/rich-text-react-renderer": "^16.1.6", + "@contentful/rich-text-types": "^17.2.5", + "@contentful/optimization-web": "0.0.0", + "contentful": "^11.10.3", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router-dom": "^7.1.0" + }, + "devDependencies": { + "@playwright/test": "^1.57.0", + "@rsbuild/core": "^1.7.3", + "@rsbuild/plugin-react": "^1.4.5", + "@types/node": "^24.0.13", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "dotenv": "^17.3.1", + "pm2": "^6.0.14", + "rimraf": "^6.1.3", + "typescript": "^5.8.3" + }, + "pnpm": { + "overrides": { + "@contentful/optimization-api-client": "file:../../pkgs/contentful-optimization-api-client-0.0.0.tgz", + "@contentful/optimization-api-schemas": "file:../../pkgs/contentful-optimization-api-schemas-0.0.0.tgz", + "@contentful/optimization-core": "file:../../pkgs/contentful-optimization-core-0.0.0.tgz", + "@contentful/optimization-web": "file:../../pkgs/contentful-optimization-web-0.0.0.tgz" + } + } +} diff --git a/implementations/web-react/playwright.config.mjs b/implementations/web-react/playwright.config.mjs new file mode 100644 index 00000000..93f2153a --- /dev/null +++ b/implementations/web-react/playwright.config.mjs @@ -0,0 +1,29 @@ +import { defineConfig, devices } from '@playwright/test' +import dotenv from 'dotenv' +import path from 'path' +import { fileURLToPath } from 'url' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +dotenv.config({ path: path.resolve(__dirname, '.env') }) + +const isCI = Boolean(process.env.CI) + +export default defineConfig({ + testDir: './e2e', + fullyParallel: true, + forbidOnly: isCI, + retries: isCI ? 2 : 0, + workers: isCI ? 1 : undefined, + reporter: [['html', { open: 'never' }]], + use: { + baseURL: process.env.PLAYWRIGHT_BASE_URL ?? 'http://localhost:3001', + trace: 'on-first-retry', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}) diff --git a/implementations/web-react/pnpm-lock.yaml b/implementations/web-react/pnpm-lock.yaml new file mode 100644 index 00000000..21e52e3e --- /dev/null +++ b/implementations/web-react/pnpm-lock.yaml @@ -0,0 +1,2145 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + '@contentful/optimization-api-client': file:../../pkgs/contentful-optimization-api-client-0.0.0.tgz + '@contentful/optimization-api-schemas': file:../../pkgs/contentful-optimization-api-schemas-0.0.0.tgz + '@contentful/optimization-core': file:../../pkgs/contentful-optimization-core-0.0.0.tgz + '@contentful/optimization-web': file:../../pkgs/contentful-optimization-web-0.0.0.tgz + +importers: + + .: + dependencies: + '@contentful/optimization-web': + specifier: file:../../pkgs/contentful-optimization-web-0.0.0.tgz + version: file:../../pkgs/contentful-optimization-web-0.0.0.tgz + '@contentful/rich-text-react-renderer': + specifier: ^16.1.6 + version: 16.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@contentful/rich-text-types': + specifier: ^17.2.5 + version: 17.2.5 + contentful: + specifier: ^11.10.3 + version: 11.10.3 + react: + specifier: ^19.2.0 + version: 19.2.4 + react-dom: + specifier: ^19.2.0 + version: 19.2.4(react@19.2.4) + react-router-dom: + specifier: ^7.1.0 + version: 7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + devDependencies: + '@playwright/test': + specifier: ^1.57.0 + version: 1.58.2 + '@rsbuild/core': + specifier: ^1.7.3 + version: 1.7.3 + '@rsbuild/plugin-react': + specifier: ^1.4.5 + version: 1.4.5(@rsbuild/core@1.7.3) + '@types/node': + specifier: ^24.0.13 + version: 24.10.13 + '@types/react': + specifier: ^19.2.7 + version: 19.2.14 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.14) + dotenv: + specifier: ^17.3.1 + version: 17.3.1 + pm2: + specifier: ^6.0.14 + version: 6.0.14 + rimraf: + specifier: ^6.1.3 + version: 6.1.3 + typescript: + specifier: ^5.8.3 + version: 5.9.3 + +packages: + + '@babel/runtime@7.28.6': + resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} + engines: {node: '>=6.9.0'} + + '@contentful/content-source-maps@0.11.44': + resolution: {integrity: sha512-tu1gfmZI59hQ7qKhmGOV0dA5brxhsc+LOfPwBsQQMDAVy5kVpTrKf/AGlL2y9QhKHu/NtCrwX/ZrmRFgtGEtIA==} + + '@contentful/optimization-api-client@file:../../pkgs/contentful-optimization-api-client-0.0.0.tgz': + resolution: {integrity: sha512-OdvgZu8rCZEvSXUA3fj0DPeqSEvuAoMBVTRZyng91KMQgeTqmEaFQqQapD9sbvJj7ePor5eyLNiWUjf4qIZ/pQ==, tarball: file:../../pkgs/contentful-optimization-api-client-0.0.0.tgz} + version: 0.0.0 + + '@contentful/optimization-api-schemas@file:../../pkgs/contentful-optimization-api-schemas-0.0.0.tgz': + resolution: {integrity: sha512-CbVtO5GX2j4nPmg1tXek1qGFmavM8vWrfFcSsgyJffOD8L65pOEuyO++W9sU3hVEg4LoxE1InSi5xOZaM7jrgg==, tarball: file:../../pkgs/contentful-optimization-api-schemas-0.0.0.tgz} + version: 0.0.0 + + '@contentful/optimization-core@file:../../pkgs/contentful-optimization-core-0.0.0.tgz': + resolution: {integrity: sha512-VVsh+cOie1KoBPj7ykJPCfK75esCsjhL27DRQEDsPPAUiXtTVV5QIX1Y98ek9zgcFiYiZWGi3S2oMAbWjteRNg==, tarball: file:../../pkgs/contentful-optimization-core-0.0.0.tgz} + version: 0.0.0 + + '@contentful/optimization-web@file:../../pkgs/contentful-optimization-web-0.0.0.tgz': + resolution: {integrity: sha512-1NqMKcimmkauJGGtb9dMqUFWK/nOM9+5xApvxeQzk815NqM32aroFbotzFnItNtJPkleWq00KELMDzxfnwHcdQ==, tarball: file:../../pkgs/contentful-optimization-web-0.0.0.tgz} + version: 0.0.0 + + '@contentful/rich-text-react-renderer@16.1.6': + resolution: {integrity: sha512-Pt0KfEnB7UP53gUKupUZjsUCHR7CiDbVyMdMmuyzYT6lNvjR7+KKYWP9eU2TOfVaXy7PxF1XEpBjSALDOHUNKQ==} + engines: {node: '>=6.0.0'} + peerDependencies: + react: ^16.8.6 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.6 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@contentful/rich-text-types@16.8.5': + resolution: {integrity: sha512-q18RJuJCOuYveGiCIjE5xLCQc5lZ3L2Qgxrlg/H2YEobDFqdtmklazRi1XwEWaK3tMg6yVXBzKKkQfLB4qW14A==} + engines: {node: '>=6.0.0'} + + '@contentful/rich-text-types@17.2.5': + resolution: {integrity: sha512-EA5vTfROZePoPmSlqLVd+luL/ev8CjnI20y6vWFVPlLRxQbv4XytXRzatydPE63CqfsPylF7NCn2z8rTLhnWfg==} + engines: {node: '>=6.0.0'} + + '@emnapi/core@1.8.1': + resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} + + '@emnapi/runtime@1.8.1': + resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} + + '@emnapi/wasi-threads@1.1.0': + resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + + '@isaacs/cliui@9.0.0': + resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==} + engines: {node: '>=18'} + + '@lingui/core@5.9.1': + resolution: {integrity: sha512-gnrh3YMo3yGI7NrYvVL5vtIap2fPahl2OAR0P9Fl/eW6GFPUGhhjIcxqxAg48/Nd4lTAzBCGwPCP+u2DzAIzEA==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@lingui/babel-plugin-lingui-macro': 5.9.1 + babel-plugin-macros: 2 || 3 + peerDependenciesMeta: + '@lingui/babel-plugin-lingui-macro': + optional: true + babel-plugin-macros: + optional: true + + '@lingui/message-utils@5.9.1': + resolution: {integrity: sha512-2hj2PqHQU7hI+2+JiViOPmeTmms/8Xl1i/AWd59hwaf+lbqDFKc8CmeZNeAvrM+D7FeYgX1/mDoQvWAkLZNYjQ==} + engines: {node: '>=20.0.0'} + + '@messageformat/parser@5.1.1': + resolution: {integrity: sha512-3p0YRGCcTUCYvBKLIxtDDyrJ0YijGIwrTRu1DT8gIviIDZru8H23+FkY6MJBzM1n9n20CiM4VeDYuBsrrwnLjg==} + + '@module-federation/error-codes@0.22.0': + resolution: {integrity: sha512-xF9SjnEy7vTdx+xekjPCV5cIHOGCkdn3pIxo9vU7gEZMIw0SvAEdsy6Uh17xaCpm8V0FWvR0SZoK9Ik6jGOaug==} + + '@module-federation/runtime-core@0.22.0': + resolution: {integrity: sha512-GR1TcD6/s7zqItfhC87zAp30PqzvceoeDGYTgF3Vx2TXvsfDrhP6Qw9T4vudDQL3uJRne6t7CzdT29YyVxlgIA==} + + '@module-federation/runtime-tools@0.22.0': + resolution: {integrity: sha512-4ScUJ/aUfEernb+4PbLdhM/c60VHl698Gn1gY21m9vyC1Ucn69fPCA1y2EwcCB7IItseRMoNhdcWQnzt/OPCNA==} + + '@module-federation/runtime@0.22.0': + resolution: {integrity: sha512-38g5iPju2tPC3KHMPxRKmy4k4onNp6ypFPS1eKGsNLUkXgHsPMBFqAjDw96iEcjri91BrahG4XcdyKi97xZzlA==} + + '@module-federation/sdk@0.22.0': + resolution: {integrity: sha512-x4aFNBKn2KVQRuNVC5A7SnrSCSqyfIWmm1DvubjbO9iKFe7ith5niw8dqSFBekYBg2Fwy+eMg4sEFNVvCAdo6g==} + + '@module-federation/webpack-bundler-runtime@0.22.0': + resolution: {integrity: sha512-aM8gCqXu+/4wBmJtVeMeeMN5guw3chf+2i6HajKtQv7SJfxV/f4IyNQJUeUQu9HfiAZHjqtMV5Lvq/Lvh8LdyA==} + + '@napi-rs/wasm-runtime@1.0.7': + resolution: {integrity: sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==} + + '@playwright/test@1.58.2': + resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==} + engines: {node: '>=18'} + hasBin: true + + '@pm2/agent@2.1.1': + resolution: {integrity: sha512-0V9ckHWd/HSC8BgAbZSoq8KXUG81X97nSkAxmhKDhmF8vanyaoc1YXwc2KVkbWz82Rg4gjd2n9qiT3i7bdvGrQ==} + + '@pm2/blessed@0.1.81': + resolution: {integrity: sha512-ZcNHqQjMuNRcQ7Z1zJbFIQZO/BDKV3KbiTckWdfbUaYhj7uNmUwb+FbdDWSCkvxNr9dBJQwvV17o6QBkAvgO0g==} + engines: {node: '>= 0.8.0'} + hasBin: true + + '@pm2/io@6.1.0': + resolution: {integrity: sha512-IxHuYURa3+FQ6BKePlgChZkqABUKFYH6Bwbw7V/pWU1pP6iR1sCI26l7P9ThUEB385ruZn/tZS3CXDUF5IA1NQ==} + engines: {node: '>=6.0'} + + '@pm2/js-api@0.8.0': + resolution: {integrity: sha512-nmWzrA/BQZik3VBz+npRcNIu01kdBhWL0mxKmP1ciF/gTcujPTQqt027N9fc1pK9ERM8RipFhymw7RcmCyOEYA==} + engines: {node: '>=4.0'} + + '@pm2/pm2-version-check@1.0.4': + resolution: {integrity: sha512-SXsM27SGH3yTWKc2fKR4SYNxsmnvuBQ9dd6QHtEWmiZ/VqaOYPAIlS8+vMcn27YLtAEBGvNRSh3TPNvtjZgfqA==} + + '@preact/signals-core@1.13.0': + resolution: {integrity: sha512-slT6XeTCAbdql61GVLlGU4x7XHI7kCZV5Um5uhE4zLX4ApgiiXc0UYFvVOKq06xcovzp7p+61l68oPi563ARKg==} + + '@rollup/rollup-linux-x64-gnu@4.57.1': + resolution: {integrity: sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==} + cpu: [x64] + os: [linux] + + '@rsbuild/core@1.7.3': + resolution: {integrity: sha512-kI1oQvCXbQYxUvQPnDLdjSX4gFsbrFNpuUj6jXEJ7IcJ74Q+n4oeFj74/8tKerhxhe0L90m/ZQfzLeN5ORGA9w==} + engines: {node: '>=18.12.0'} + hasBin: true + + '@rsbuild/plugin-react@1.4.5': + resolution: {integrity: sha512-eS2sXCedgGA/7bLu8yVtn48eE/GyPbXx4Q7OcutB01IQ1D2y8WSMBys4nwfrecy19utvw4NPn4gYDy52316+vg==} + peerDependencies: + '@rsbuild/core': ^1.0.0 || ^2.0.0-0 + + '@rspack/binding-darwin-arm64@1.7.6': + resolution: {integrity: sha512-NZ9AWtB1COLUX1tA9HQQvWpTy07NSFfKBU8A6ylWd5KH8AePZztpNgLLAVPTuNO4CZXYpwcoclf8jG/luJcQdQ==} + cpu: [arm64] + os: [darwin] + + '@rspack/binding-darwin-x64@1.7.6': + resolution: {integrity: sha512-J2g6xk8ZS7uc024dNTGTHxoFzFovAZIRixUG7PiciLKTMP78svbSSWrmW6N8oAsAkzYfJWwQpVgWfFNRHvYxSw==} + cpu: [x64] + os: [darwin] + + '@rspack/binding-linux-arm64-gnu@1.7.6': + resolution: {integrity: sha512-eQfcsaxhFrv5FmtaA7+O1F9/2yFDNIoPZzV/ZvqvFz5bBXVc4FAm/1fVpBg8Po/kX1h0chBc7Xkpry3cabFW8w==} + cpu: [arm64] + os: [linux] + + '@rspack/binding-linux-arm64-musl@1.7.6': + resolution: {integrity: sha512-DfQXKiyPIl7i1yECHy4eAkSmlUzzsSAbOjgMuKn7pudsWf483jg0UUYutNgXSlBjc/QSUp7906Cg8oty9OfwPA==} + cpu: [arm64] + os: [linux] + + '@rspack/binding-linux-x64-gnu@1.7.6': + resolution: {integrity: sha512-NdA+2X3lk2GGrMMnTGyYTzM3pn+zNjaqXqlgKmFBXvjfZqzSsKq3pdD1KHZCd5QHN+Fwvoszj0JFsquEVhE1og==} + cpu: [x64] + os: [linux] + + '@rspack/binding-linux-x64-musl@1.7.6': + resolution: {integrity: sha512-rEy6MHKob02t/77YNgr6dREyJ0e0tv1X6Xsg8Z5E7rPXead06zefUbfazj4RELYySWnM38ovZyJAkPx/gOn3VA==} + cpu: [x64] + os: [linux] + + '@rspack/binding-wasm32-wasi@1.7.6': + resolution: {integrity: sha512-YupOrz0daSG+YBbCIgpDgzfMM38YpChv+afZpaxx5Ml7xPeAZIIdgWmLHnQ2rts73N2M1NspAiBwV00Xx0N4Vg==} + cpu: [wasm32] + + '@rspack/binding-win32-arm64-msvc@1.7.6': + resolution: {integrity: sha512-INj7aVXjBvlZ84kEhSK4kJ484ub0i+BzgnjDWOWM1K+eFYDZjLdAsQSS3fGGXwVc3qKbPIssFfnftATDMTEJHQ==} + cpu: [arm64] + os: [win32] + + '@rspack/binding-win32-ia32-msvc@1.7.6': + resolution: {integrity: sha512-lXGvC+z67UMcw58In12h8zCa9IyYRmuptUBMItQJzu+M278aMuD1nETyGLL7e4+OZ2lvrnnBIcjXN1hfw2yRzw==} + cpu: [ia32] + os: [win32] + + '@rspack/binding-win32-x64-msvc@1.7.6': + resolution: {integrity: sha512-zeUxEc0ZaPpmaYlCeWcjSJUPuRRySiSHN23oJ2Xyw0jsQ01Qm4OScPdr0RhEOFuK/UE+ANyRtDo4zJsY52Hadw==} + cpu: [x64] + os: [win32] + + '@rspack/binding@1.7.6': + resolution: {integrity: sha512-/NrEcfo8Gx22hLGysanrV6gHMuqZSxToSci/3M4kzEQtF5cPjfOv5pqeLK/+B6cr56ul/OmE96cCdWcXeVnFjQ==} + + '@rspack/core@1.7.6': + resolution: {integrity: sha512-Iax6UhrfZqJajA778c1d5DBFbSIqPOSrI34kpNIiNpWd8Jq7mFIa+Z60SQb5ZQDZuUxcCZikjz5BxinFjTkg7Q==} + engines: {node: '>=18.12.0'} + peerDependencies: + '@swc/helpers': '>=0.5.1' + peerDependenciesMeta: + '@swc/helpers': + optional: true + + '@rspack/lite-tapable@1.1.0': + resolution: {integrity: sha512-E2B0JhYFmVAwdDiG14+DW0Di4Ze4Jg10Pc4/lILUrd5DRCaklduz2OvJ5HYQ6G+hd+WTzqQb3QnDNfK4yvAFYw==} + + '@rspack/plugin-react-refresh@1.6.1': + resolution: {integrity: sha512-eqqW5645VG3CzGzFgNg5HqNdHVXY+567PGjtDhhrM8t67caxmsSzRmT5qfoEIfBcGgFkH9vEg7kzXwmCYQdQDw==} + peerDependencies: + react-refresh: '>=0.10.0 <1.0.0' + webpack-hot-middleware: 2.x + peerDependenciesMeta: + webpack-hot-middleware: + optional: true + + '@swc/helpers@0.5.18': + resolution: {integrity: sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==} + + '@tootallnate/quickjs-emscripten@0.23.0': + resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/js-cookie@3.0.6': + resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==} + + '@types/node@24.10.13': + resolution: {integrity: sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + + '@types/retry@0.12.2': + resolution: {integrity: sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==} + + '@vercel/stega@0.1.2': + resolution: {integrity: sha512-P7mafQXjkrsoyTRppnt0N21udKS9wUmLXHRyP9saLXLHw32j/FgUJ3FscSWgvSqRs4cj7wKZtwqJEvWJ2jbGmA==} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + amp-message@0.1.2: + resolution: {integrity: sha512-JqutcFwoU1+jhv7ArgW38bqrE+LQdcRv4NxNw0mp0JHQyB6tXesWRjtYKlDgHRY2o3JE5UTaBGUK8kSWUdxWUg==} + + amp@0.3.1: + resolution: {integrity: sha512-OwIuC4yZaRogHKiuU5WlMR5Xk/jAcpPtawWL05Gj8Lvm2F6mwoJt4O/bHI+DHwG79vWd+8OFYM4/BzYqyRd3qw==} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansis@4.0.0-node10: + resolution: {integrity: sha512-BRrU0Bo1X9dFGw6KgGz6hWrqQuOlVEDOzkb0QSLZY9sXHqA7pNj7yHPVJRz7y/rj4EOJ3d/D5uxH+ee9leYgsg==} + engines: {node: '>=10'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + ast-types@0.13.4: + resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} + engines: {node: '>=4'} + + async@2.6.4: + resolution: {integrity: sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.13.5: + resolution: {integrity: sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==} + + balanced-match@4.0.2: + resolution: {integrity: sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==} + engines: {node: 20 || >=22} + + basic-ftp@5.1.0: + resolution: {integrity: sha512-RkaJzeJKDbaDWTIPiJwubyljaEPwpVWkm9Rt5h9Nd6h7tEXTJ3VB4qxdZBioV7JO5yLUaOKwz7vDOzlncUsegw==} + engines: {node: '>=10.0.0'} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + bodec@0.1.0: + resolution: {integrity: sha512-Ylo+MAo5BDUq1KA3f3R/MFhh+g8cnHmo8bz3YPGhI1znrMaf77ol1sfvYJzsw3nTE+Y2GryfDxBaR+AqpAkEHQ==} + + brace-expansion@5.0.2: + resolution: {integrity: sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==} + engines: {node: 20 || >=22} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + chalk@3.0.0: + resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} + engines: {node: '>=8'} + + charm@0.1.2: + resolution: {integrity: sha512-syedaZ9cPe7r3hoQA9twWYKu5AIyCswN5+szkmPBe9ccdLrj4bYaCnLVPTLd2kgVRc7+zoX4tyPgRnFKCj5YjQ==} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + cli-tableau@2.0.1: + resolution: {integrity: sha512-he+WTicka9cl0Fg/y+YyxcN6/bfQ/1O3QmgxRXDhABKqLzvoOSM4fMzp39uMyLBulAFuywD2N7UaoQE7WaADxQ==} + engines: {node: '>=8.10.0'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@2.15.1: + resolution: {integrity: sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==} + + contentful-resolve-response@1.9.5: + resolution: {integrity: sha512-1l0iG5avoi+Hy1GVQ7ELoG9MWdbCVJDlOVnLCB0B/6YDQNuD4qAU7EejRki6+skX/Eqswq1KSjqgd51uah3YDA==} + engines: {node: '>=4.7.2'} + + contentful-sdk-core@9.4.2: + resolution: {integrity: sha512-G/8cEs3ggeMrUtT8ywXlzoSCKNpmUo2ZXq/pBCnxcM6JP7isfk8nYDHL0xRR9qTwlNs3XCmCf20iApOQeR6Qsg==} + engines: {node: '>=18'} + + contentful@11.10.3: + resolution: {integrity: sha512-oDH+wsYypYQnOIjBqyAKEIbzxDvaICpk8X52fDmtadIY6TFGWS9ar8MJYWvfW4MDiHLCNfaGAjS4S5KOuSecaw==} + engines: {node: '>=18'} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + core-js@3.47.0: + resolution: {integrity: sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==} + + croner@4.1.97: + resolution: {integrity: sha512-/f6gpQuxDaqXu+1kwQYSckUglPaOrHdbIlBAu0YuW8/Cdb45XwXYNUBXg3r/9Mo6n540Kn/smKcZWko5x99KrQ==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + culvert@0.1.2: + resolution: {integrity: sha512-yi1x3EAWKjQTreYWeSd98431AV+IEE0qoDyOoaHJ7KJ21gv6HtBXHVLX74opVSGqcR8/AbjJBHAHpcOy2bj5Gg==} + + data-uri-to-buffer@6.0.2: + resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} + engines: {node: '>= 14'} + + dayjs@1.11.15: + resolution: {integrity: sha512-MC+DfnSWiM9APs7fpiurHGCoeIx0Gdl6QZBy+5lu8MbYKN5FZEXqOgrundfibdfhGZ15o9hzmZ2xJjZnbvgKXQ==} + + dayjs@1.8.36: + resolution: {integrity: sha512-3VmRXEtw7RZKAf+4Tv1Ym9AGeo8r8+CjDi26x+7SYQil1UqtqdaokhzoEJohqlzt0m5kacJSDhJQkG/LWhpRBw==} + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + degenerator@5.0.1: + resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} + engines: {node: '>= 14'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + diary@0.4.5: + resolution: {integrity: sha512-dUtG/AVG5bt9Mi+23TgTvjZ0NDJaszjs1GpYooM5cbEzk2xoqdvxCOlVw0xkenQXZw/DFxp23tj5VkP6YmlRmw==} + + dotenv@17.3.1: + resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + enquirer@2.3.6: + resolution: {integrity: sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==} + engines: {node: '>=8.6'} + + error-stack-parser@2.1.4: + resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-toolkit@1.44.0: + resolution: {integrity: sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + eventemitter2@5.0.1: + resolution: {integrity: sha512-5EM1GHXycJBS6mauYAbVKT1cVs7POKWb2NXD4Vyt8dDqeZa7LaDK1/sjtL+Zb0lzTpSNil4596Dyu97hz37QLg==} + + eventemitter2@6.4.9: + resolution: {integrity: sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==} + + extrareqp2@1.0.0: + resolution: {integrity: sha512-Gum0g1QYb6wpPJCVypWP3bbIuaibcFiJcpuPM10YSXp/tzqi84x9PJageob+eN4xVRIOto4wjSGNLyMD54D2xA==} + + fast-copy@3.0.2: + resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==} + + fast-json-patch@3.1.1: + resolution: {integrity: sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==} + + fclone@1.0.11: + resolution: {integrity: sha512-GDqVQezKzRABdeqflsgMr7ktzgF9CyS+p2oe0jJqUY6izSSbhPIQJDpoU4PtGcD7VPM9xh/dVrTu6z1nwgmEGw==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + foreach@2.0.6: + resolution: {integrity: sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg==} + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-uri@6.0.5: + resolution: {integrity: sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==} + engines: {node: '>= 14'} + + git-node-fs@1.0.0: + resolution: {integrity: sha512-bLQypt14llVXBg0S0u8q8HmU7g9p3ysH+NvVlae5vILuUvs759665HvmR5+wb04KjHyjFcDRxdYb4kyNnluMUQ==} + peerDependencies: + js-git: ^0.7.8 + peerDependenciesMeta: + js-git: + optional: true + + git-sha1@0.1.2: + resolution: {integrity: sha512-2e/nZezdVlyCopOCYHeW0onkbZg7xP1Ad6pndPy1rCygeRykefUS6r7oA5cJRGEFvseiaz5a/qUHFVX1dd6Isg==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob@13.0.4: + resolution: {integrity: sha512-KACie1EOs9BIOMtenFaxwmYODWA3/fTfGSUnLhMJpXRntu1g+uL/Xvub5f8SCTppvo9q62Qy4LeOoUiaL54G5A==} + engines: {node: 20 || >=22} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + html-entities@2.6.0: + resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + ip-address@10.1.0: + resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} + engines: {node: '>= 12'} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-network-error@1.3.0: + resolution: {integrity: sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw==} + engines: {node: '>=16'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-plain-obj@3.0.0: + resolution: {integrity: sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==} + engines: {node: '>=10'} + + jackspeak@4.2.3: + resolution: {integrity: sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==} + engines: {node: 20 || >=22} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} + + js-git@0.7.8: + resolution: {integrity: sha512-+E5ZH/HeRnoc/LW0AmAyhU+mNcWBzAKE+30+IDMLSLbbK+Tdt02AdkOKq9u15rlJsDEGFqtgckc8ZM59LhhiUA==} + + js-sha256@0.10.1: + resolution: {integrity: sha512-5obBtsz9301ULlsgggLg542s/jqtddfOpV5KJc4hajc9JV8GeY2gZHSVpYBn4nWqAUTJ9v+xwtbJ1mIBgIH5Vw==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + json-pointer@0.6.2: + resolution: {integrity: sha512-vLWcKbOaXlO+jvRy4qNd+TI1QUPZzfJj1tpJ3vAXDych5XJf93ftpUKe5pKCrzyIIwgBJcOcCVRUfqQP25afBw==} + + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + + lodash@4.17.23: + resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + + lru-cache@11.2.6: + resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==} + engines: {node: 20 || >=22} + + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + + lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + minimatch@10.2.1: + resolution: {integrity: sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==} + engines: {node: 20 || >=22} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + + module-details-from-path@1.0.4: + resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} + + moo@0.5.2: + resolution: {integrity: sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mute-stream@0.0.8: + resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} + + needle@2.4.0: + resolution: {integrity: sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg==} + engines: {node: '>= 4.4.x'} + hasBin: true + + netmask@2.0.2: + resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} + engines: {node: '>= 0.4.0'} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + p-retry@6.2.1: + resolution: {integrity: sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==} + engines: {node: '>=16.17'} + + p-retry@7.1.1: + resolution: {integrity: sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w==} + engines: {node: '>=20'} + + pac-proxy-agent@7.2.0: + resolution: {integrity: sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==} + engines: {node: '>= 14'} + + pac-resolver@7.0.1: + resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==} + engines: {node: '>= 14'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + pako@0.2.9: + resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@2.0.1: + resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} + engines: {node: 20 || >=22} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + pidusage@2.0.21: + resolution: {integrity: sha512-cv3xAQos+pugVX+BfXpHsbyz/dLzX+lr44zNMsYiGxUw+kV5sgQCIcLd1z+0vq+KyC7dJ+/ts2PsfgWfSC3WXA==} + engines: {node: '>=8'} + + pidusage@3.0.2: + resolution: {integrity: sha512-g0VU+y08pKw5M8EZ2rIGiEBaB8wrQMjYGFfW2QVIfyT8V+fq8YFLkvlz4bz5ljvFDJYNFCWT3PWqcRr2FKO81w==} + engines: {node: '>=10'} + + playwright-core@1.58.2: + resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.58.2: + resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==} + engines: {node: '>=18'} + hasBin: true + + pm2-axon-rpc@0.7.1: + resolution: {integrity: sha512-FbLvW60w+vEyvMjP/xom2UPhUN/2bVpdtLfKJeYM3gwzYhoTEEChCOICfFzxkxuoEleOlnpjie+n1nue91bDQw==} + engines: {node: '>=5'} + + pm2-axon@4.0.1: + resolution: {integrity: sha512-kES/PeSLS8orT8dR5jMlNl+Yu4Ty3nbvZRmaAtROuVm9nYYGiaoXqqKQqQYzWQzMYWUKHMQTvBlirjE5GIIxqg==} + engines: {node: '>=5'} + + pm2-deploy@1.0.2: + resolution: {integrity: sha512-YJx6RXKrVrWaphEYf++EdOOx9EH18vM8RSZN/P1Y+NokTKqYAca/ejXwVLyiEpNju4HPZEk3Y2uZouwMqUlcgg==} + engines: {node: '>=4.0.0'} + + pm2-multimeter@0.1.2: + resolution: {integrity: sha512-S+wT6XfyKfd7SJIBqRgOctGxaBzUOmVQzTAS+cg04TsEUObJVreha7lvCfX8zzGVr871XwCSnHUU7DQQ5xEsfA==} + + pm2-sysmonit@1.2.8: + resolution: {integrity: sha512-ACOhlONEXdCTVwKieBIQLSi2tQZ8eKinhcr9JpZSUAL8Qy0ajIgRtsLxG/lwPOW3JEKqPyw/UaHmTWhUzpP4kA==} + + pm2@6.0.14: + resolution: {integrity: sha512-wX1FiFkzuT2H/UUEA8QNXDAA9MMHDsK/3UHj6Dkd5U7kxyigKDA5gyDw78ycTQZAuGCLWyUX5FiXEuVQWafukA==} + engines: {node: '>=16.0.0'} + hasBin: true + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + + promptly@2.2.0: + resolution: {integrity: sha512-aC9j+BZsRSSzEsXBNBwDnAxujdx19HycZoKgRgzWnS8eOHg1asuf9heuLprfbe739zY3IdUQx+Egv6Jn135WHA==} + + proxy-agent@6.4.0: + resolution: {integrity: sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==} + engines: {node: '>= 14'} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + qs@6.15.0: + resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} + engines: {node: '>=0.6'} + + react-dom@19.2.4: + resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} + peerDependencies: + react: ^19.2.4 + + react-refresh@0.18.0: + resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} + engines: {node: '>=0.10.0'} + + react-router-dom@7.13.0: + resolution: {integrity: sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router@7.13.0: + resolution: {integrity: sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + + react@19.2.4: + resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} + engines: {node: '>=0.10.0'} + + read@1.0.7: + resolution: {integrity: sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==} + engines: {node: '>=0.8'} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + require-in-the-middle@5.2.0: + resolution: {integrity: sha512-efCx3b+0Z69/LGJmm9Yvi4cqEdxnoGnxYxGxBghkkTTFeXRtTCmmhO0AnAfHz59k957uTSuy8WaHqOs8wbYUWg==} + engines: {node: '>=6'} + + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + + retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + + rimraf@6.1.3: + resolution: {integrity: sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==} + engines: {node: 20 || >=22} + hasBin: true + + run-series@1.1.9: + resolution: {integrity: sha512-Arc4hUN896vjkqCYrUXquBFtRZdv1PfLbTYP71efP6butxyQ0kWpiNJyAgsxscmQg1cqvHY32/UCBzXedTpU2g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sax@1.4.4: + resolution: {integrity: sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==} + engines: {node: '>=11.0.0'} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@7.5.4: + resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} + engines: {node: '>=10'} + hasBin: true + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + + shimmer@1.2.1: + resolution: {integrity: sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + socks-proxy-agent@8.0.5: + resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} + engines: {node: '>= 14'} + + socks@2.8.7: + resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + sprintf-js@1.1.2: + resolution: {integrity: sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==} + + stackframe@1.3.4: + resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + systeminformation@5.31.0: + resolution: {integrity: sha512-z5pjzvC8UnQJ/iu34z+mo3lAeMzTGdArjPQoG5uPyV5XY4BY+M6ZcRTl4XnZqudz6sP713LhWMKv6e0kGFGCgQ==} + engines: {node: '>=8.0.0'} + os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android] + hasBin: true + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tslib@1.9.3: + resolution: {integrity: sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tv4@1.3.0: + resolution: {integrity: sha512-afizzfpJgvPr+eDkREK4MxJ/+r8nEEHcmitwgnPUqpaP+FpwQyadnxNoSACbgc/b1LsZYtODGoPiFxQrgJgjvw==} + engines: {node: '>= 0.8.0'} + + tx2@1.0.5: + resolution: {integrity: sha512-sJ24w0y03Md/bxzK4FU8J8JveYYUbSs2FViLJ2D/8bytSiyPRbuE3DyL/9UKYXTZlV3yXq0L8GLlhobTnekCVg==} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + vizion@2.2.1: + resolution: {integrity: sha512-sfAcO2yeSU0CSPFI/DmZp3FsFE9T+8913nv1xWBOyzODv13fwkn6Vl7HqxGpkr9F608M+8SuFId3s+BlZqfXww==} + engines: {node: '>=4.0'} + + ws@7.5.10: + resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + +snapshots: + + '@babel/runtime@7.28.6': {} + + '@contentful/content-source-maps@0.11.44': + dependencies: + '@vercel/stega': 0.1.2 + json-pointer: 0.6.2 + + '@contentful/optimization-api-client@file:../../pkgs/contentful-optimization-api-client-0.0.0.tgz': + dependencies: + '@contentful/optimization-api-schemas': file:../../pkgs/contentful-optimization-api-schemas-0.0.0.tgz + diary: 0.4.5 + es-toolkit: 1.44.0 + p-retry: 6.2.1 + zod: 4.3.6 + transitivePeerDependencies: + - debug + + '@contentful/optimization-api-schemas@file:../../pkgs/contentful-optimization-api-schemas-0.0.0.tgz': + dependencies: + contentful: 11.10.3 + zod: 4.3.6 + transitivePeerDependencies: + - debug + + '@contentful/optimization-core@file:../../pkgs/contentful-optimization-core-0.0.0.tgz': + dependencies: + '@contentful/optimization-api-client': file:../../pkgs/contentful-optimization-api-client-0.0.0.tgz + '@preact/signals-core': 1.13.0 + contentful: 11.10.3 + diary: 0.4.5 + es-toolkit: 1.44.0 + p-retry: 7.1.1 + zod: 4.3.6 + transitivePeerDependencies: + - debug + + '@contentful/optimization-web@file:../../pkgs/contentful-optimization-web-0.0.0.tgz': + dependencies: + '@contentful/optimization-core': file:../../pkgs/contentful-optimization-core-0.0.0.tgz + '@types/js-cookie': 3.0.6 + es-toolkit: 1.44.0 + js-cookie: 3.0.5 + zod: 4.3.6 + transitivePeerDependencies: + - debug + + '@contentful/rich-text-react-renderer@16.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@contentful/rich-text-types': 17.2.5 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + transitivePeerDependencies: + - '@lingui/babel-plugin-lingui-macro' + - babel-plugin-macros + + '@contentful/rich-text-types@16.8.5': {} + + '@contentful/rich-text-types@17.2.5': + dependencies: + '@lingui/core': 5.9.1 + is-plain-obj: 3.0.0 + transitivePeerDependencies: + - '@lingui/babel-plugin-lingui-macro' + - babel-plugin-macros + + '@emnapi/core@1.8.1': + dependencies: + '@emnapi/wasi-threads': 1.1.0 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.8.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.1.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@isaacs/cliui@9.0.0': {} + + '@lingui/core@5.9.1': + dependencies: + '@babel/runtime': 7.28.6 + '@lingui/message-utils': 5.9.1 + + '@lingui/message-utils@5.9.1': + dependencies: + '@messageformat/parser': 5.1.1 + js-sha256: 0.10.1 + + '@messageformat/parser@5.1.1': + dependencies: + moo: 0.5.2 + + '@module-federation/error-codes@0.22.0': {} + + '@module-federation/runtime-core@0.22.0': + dependencies: + '@module-federation/error-codes': 0.22.0 + '@module-federation/sdk': 0.22.0 + + '@module-federation/runtime-tools@0.22.0': + dependencies: + '@module-federation/runtime': 0.22.0 + '@module-federation/webpack-bundler-runtime': 0.22.0 + + '@module-federation/runtime@0.22.0': + dependencies: + '@module-federation/error-codes': 0.22.0 + '@module-federation/runtime-core': 0.22.0 + '@module-federation/sdk': 0.22.0 + + '@module-federation/sdk@0.22.0': {} + + '@module-federation/webpack-bundler-runtime@0.22.0': + dependencies: + '@module-federation/runtime': 0.22.0 + '@module-federation/sdk': 0.22.0 + + '@napi-rs/wasm-runtime@1.0.7': + dependencies: + '@emnapi/core': 1.8.1 + '@emnapi/runtime': 1.8.1 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@playwright/test@1.58.2': + dependencies: + playwright: 1.58.2 + + '@pm2/agent@2.1.1': + dependencies: + async: 3.2.6 + chalk: 3.0.0 + dayjs: 1.8.36 + debug: 4.3.7 + eventemitter2: 5.0.1 + fast-json-patch: 3.1.1 + fclone: 1.0.11 + pm2-axon: 4.0.1 + pm2-axon-rpc: 0.7.1 + proxy-agent: 6.4.0 + semver: 7.5.4 + ws: 7.5.10 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@pm2/blessed@0.1.81': {} + + '@pm2/io@6.1.0': + dependencies: + async: 2.6.4 + debug: 4.3.7 + eventemitter2: 6.4.9 + require-in-the-middle: 5.2.0 + semver: 7.5.4 + shimmer: 1.2.1 + signal-exit: 3.0.7 + tslib: 1.9.3 + transitivePeerDependencies: + - supports-color + + '@pm2/js-api@0.8.0': + dependencies: + async: 2.6.4 + debug: 4.3.7 + eventemitter2: 6.4.9 + extrareqp2: 1.0.0(debug@4.3.7) + ws: 7.5.10 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@pm2/pm2-version-check@1.0.4': + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@preact/signals-core@1.13.0': {} + + '@rollup/rollup-linux-x64-gnu@4.57.1': + optional: true + + '@rsbuild/core@1.7.3': + dependencies: + '@rspack/core': 1.7.6(@swc/helpers@0.5.18) + '@rspack/lite-tapable': 1.1.0 + '@swc/helpers': 0.5.18 + core-js: 3.47.0 + jiti: 2.6.1 + + '@rsbuild/plugin-react@1.4.5(@rsbuild/core@1.7.3)': + dependencies: + '@rsbuild/core': 1.7.3 + '@rspack/plugin-react-refresh': 1.6.1(react-refresh@0.18.0) + react-refresh: 0.18.0 + transitivePeerDependencies: + - webpack-hot-middleware + + '@rspack/binding-darwin-arm64@1.7.6': + optional: true + + '@rspack/binding-darwin-x64@1.7.6': + optional: true + + '@rspack/binding-linux-arm64-gnu@1.7.6': + optional: true + + '@rspack/binding-linux-arm64-musl@1.7.6': + optional: true + + '@rspack/binding-linux-x64-gnu@1.7.6': + optional: true + + '@rspack/binding-linux-x64-musl@1.7.6': + optional: true + + '@rspack/binding-wasm32-wasi@1.7.6': + dependencies: + '@napi-rs/wasm-runtime': 1.0.7 + optional: true + + '@rspack/binding-win32-arm64-msvc@1.7.6': + optional: true + + '@rspack/binding-win32-ia32-msvc@1.7.6': + optional: true + + '@rspack/binding-win32-x64-msvc@1.7.6': + optional: true + + '@rspack/binding@1.7.6': + optionalDependencies: + '@rspack/binding-darwin-arm64': 1.7.6 + '@rspack/binding-darwin-x64': 1.7.6 + '@rspack/binding-linux-arm64-gnu': 1.7.6 + '@rspack/binding-linux-arm64-musl': 1.7.6 + '@rspack/binding-linux-x64-gnu': 1.7.6 + '@rspack/binding-linux-x64-musl': 1.7.6 + '@rspack/binding-wasm32-wasi': 1.7.6 + '@rspack/binding-win32-arm64-msvc': 1.7.6 + '@rspack/binding-win32-ia32-msvc': 1.7.6 + '@rspack/binding-win32-x64-msvc': 1.7.6 + + '@rspack/core@1.7.6(@swc/helpers@0.5.18)': + dependencies: + '@module-federation/runtime-tools': 0.22.0 + '@rspack/binding': 1.7.6 + '@rspack/lite-tapable': 1.1.0 + optionalDependencies: + '@swc/helpers': 0.5.18 + + '@rspack/lite-tapable@1.1.0': {} + + '@rspack/plugin-react-refresh@1.6.1(react-refresh@0.18.0)': + dependencies: + error-stack-parser: 2.1.4 + html-entities: 2.6.0 + react-refresh: 0.18.0 + + '@swc/helpers@0.5.18': + dependencies: + tslib: 2.8.1 + + '@tootallnate/quickjs-emscripten@0.23.0': {} + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/js-cookie@3.0.6': {} + + '@types/node@24.10.13': + dependencies: + undici-types: 7.16.0 + + '@types/react-dom@19.2.3(@types/react@19.2.14)': + dependencies: + '@types/react': 19.2.14 + + '@types/react@19.2.14': + dependencies: + csstype: 3.2.3 + + '@types/retry@0.12.2': {} + + '@vercel/stega@0.1.2': {} + + agent-base@7.1.4: {} + + amp-message@0.1.2: + dependencies: + amp: 0.3.1 + + amp@0.3.1: {} + + ansi-colors@4.1.3: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansis@4.0.0-node10: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + argparse@2.0.1: {} + + ast-types@0.13.4: + dependencies: + tslib: 2.8.1 + + async@2.6.4: + dependencies: + lodash: 4.17.23 + + async@3.2.6: {} + + asynckit@0.4.0: {} + + axios@1.13.5: + dependencies: + follow-redirects: 1.15.11(debug@4.3.7) + form-data: 4.0.5 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + balanced-match@4.0.2: + dependencies: + jackspeak: 4.2.3 + + basic-ftp@5.1.0: {} + + binary-extensions@2.3.0: {} + + bodec@0.1.0: {} + + brace-expansion@5.0.2: + dependencies: + balanced-match: 4.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + buffer-from@1.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + chalk@3.0.0: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + charm@0.1.2: {} + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + cli-tableau@2.0.1: + dependencies: + chalk: 3.0.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@2.15.1: {} + + contentful-resolve-response@1.9.5: + dependencies: + fast-copy: 3.0.2 + + contentful-sdk-core@9.4.2: + dependencies: + fast-copy: 3.0.2 + lodash: 4.17.23 + process: 0.11.10 + qs: 6.15.0 + optionalDependencies: + '@rollup/rollup-linux-x64-gnu': 4.57.1 + + contentful@11.10.3: + dependencies: + '@contentful/content-source-maps': 0.11.44 + '@contentful/rich-text-types': 16.8.5 + axios: 1.13.5 + contentful-resolve-response: 1.9.5 + contentful-sdk-core: 9.4.2 + json-stringify-safe: 5.0.1 + tslib: 2.8.1 + type-fest: 4.41.0 + transitivePeerDependencies: + - debug + + cookie@1.1.1: {} + + core-js@3.47.0: {} + + croner@4.1.97: {} + + csstype@3.2.3: {} + + culvert@0.1.2: {} + + data-uri-to-buffer@6.0.2: {} + + dayjs@1.11.15: {} + + dayjs@1.8.36: {} + + debug@3.2.7: + dependencies: + ms: 2.1.3 + + debug@4.3.7: + dependencies: + ms: 2.1.3 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + degenerator@5.0.1: + dependencies: + ast-types: 0.13.4 + escodegen: 2.1.0 + esprima: 4.0.1 + + delayed-stream@1.0.0: {} + + diary@0.4.5: {} + + dotenv@17.3.1: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + enquirer@2.3.6: + dependencies: + ansi-colors: 4.1.3 + + error-stack-parser@2.1.4: + dependencies: + stackframe: 1.3.4 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-toolkit@1.44.0: {} + + escape-string-regexp@4.0.0: {} + + escodegen@2.1.0: + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + + esprima@4.0.1: {} + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + eventemitter2@5.0.1: {} + + eventemitter2@6.4.9: {} + + extrareqp2@1.0.0(debug@4.3.7): + dependencies: + follow-redirects: 1.15.11(debug@4.3.7) + transitivePeerDependencies: + - debug + + fast-copy@3.0.2: {} + + fast-json-patch@3.1.1: {} + + fclone@1.0.11: {} + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + follow-redirects@1.15.11(debug@4.3.7): + optionalDependencies: + debug: 4.3.7 + + foreach@2.0.6: {} + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + fsevents@2.3.2: + optional: true + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-uri@6.0.5: + dependencies: + basic-ftp: 5.1.0 + data-uri-to-buffer: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + git-node-fs@1.0.0(js-git@0.7.8): + optionalDependencies: + js-git: 0.7.8 + + git-sha1@0.1.2: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob@13.0.4: + dependencies: + minimatch: 10.2.1 + minipass: 7.1.2 + path-scurry: 2.0.1 + + gopd@1.2.0: {} + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + html-entities@2.6.0: {} + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + ini@1.3.8: {} + + ip-address@10.1.0: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-network-error@1.3.0: {} + + is-number@7.0.0: {} + + is-plain-obj@3.0.0: {} + + jackspeak@4.2.3: + dependencies: + '@isaacs/cliui': 9.0.0 + + jiti@2.6.1: {} + + js-cookie@3.0.5: {} + + js-git@0.7.8: + dependencies: + bodec: 0.1.0 + culvert: 0.1.2 + git-sha1: 0.1.2 + pako: 0.2.9 + + js-sha256@0.10.1: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + json-pointer@0.6.2: + dependencies: + foreach: 2.0.6 + + json-stringify-safe@5.0.1: {} + + lodash@4.17.23: {} + + lru-cache@11.2.6: {} + + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + + lru-cache@7.18.3: {} + + math-intrinsics@1.1.0: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + minimatch@10.2.1: + dependencies: + brace-expansion: 5.0.2 + + minipass@7.1.2: {} + + mkdirp@1.0.4: {} + + module-details-from-path@1.0.4: {} + + moo@0.5.2: {} + + ms@2.1.3: {} + + mute-stream@0.0.8: {} + + needle@2.4.0: + dependencies: + debug: 3.2.7 + iconv-lite: 0.4.24 + sax: 1.4.4 + transitivePeerDependencies: + - supports-color + + netmask@2.0.2: {} + + normalize-path@3.0.0: {} + + object-inspect@1.13.4: {} + + p-retry@6.2.1: + dependencies: + '@types/retry': 0.12.2 + is-network-error: 1.3.0 + retry: 0.13.1 + + p-retry@7.1.1: + dependencies: + is-network-error: 1.3.0 + + pac-proxy-agent@7.2.0: + dependencies: + '@tootallnate/quickjs-emscripten': 0.23.0 + agent-base: 7.1.4 + debug: 4.4.3 + get-uri: 6.0.5 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + pac-resolver: 7.0.1 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + + pac-resolver@7.0.1: + dependencies: + degenerator: 5.0.1 + netmask: 2.0.2 + + package-json-from-dist@1.0.1: {} + + pako@0.2.9: {} + + path-parse@1.0.7: {} + + path-scurry@2.0.1: + dependencies: + lru-cache: 11.2.6 + minipass: 7.1.2 + + picomatch@2.3.1: {} + + pidusage@2.0.21: + dependencies: + safe-buffer: 5.2.1 + optional: true + + pidusage@3.0.2: + dependencies: + safe-buffer: 5.2.1 + + playwright-core@1.58.2: {} + + playwright@1.58.2: + dependencies: + playwright-core: 1.58.2 + optionalDependencies: + fsevents: 2.3.2 + + pm2-axon-rpc@0.7.1: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + pm2-axon@4.0.1: + dependencies: + amp: 0.3.1 + amp-message: 0.1.2 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + transitivePeerDependencies: + - supports-color + + pm2-deploy@1.0.2: + dependencies: + run-series: 1.1.9 + tv4: 1.3.0 + + pm2-multimeter@0.1.2: + dependencies: + charm: 0.1.2 + + pm2-sysmonit@1.2.8: + dependencies: + async: 3.2.6 + debug: 4.4.3 + pidusage: 2.0.21 + systeminformation: 5.31.0 + tx2: 1.0.5 + transitivePeerDependencies: + - supports-color + optional: true + + pm2@6.0.14: + dependencies: + '@pm2/agent': 2.1.1 + '@pm2/blessed': 0.1.81 + '@pm2/io': 6.1.0 + '@pm2/js-api': 0.8.0 + '@pm2/pm2-version-check': 1.0.4 + ansis: 4.0.0-node10 + async: 3.2.6 + chokidar: 3.6.0 + cli-tableau: 2.0.1 + commander: 2.15.1 + croner: 4.1.97 + dayjs: 1.11.15 + debug: 4.4.3 + enquirer: 2.3.6 + eventemitter2: 5.0.1 + fclone: 1.0.11 + js-yaml: 4.1.1 + mkdirp: 1.0.4 + needle: 2.4.0 + pidusage: 3.0.2 + pm2-axon: 4.0.1 + pm2-axon-rpc: 0.7.1 + pm2-deploy: 1.0.2 + pm2-multimeter: 0.1.2 + promptly: 2.2.0 + semver: 7.7.2 + source-map-support: 0.5.21 + sprintf-js: 1.1.2 + vizion: 2.2.1 + optionalDependencies: + pm2-sysmonit: 1.2.8 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + process@0.11.10: {} + + promptly@2.2.0: + dependencies: + read: 1.0.7 + + proxy-agent@6.4.0: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + lru-cache: 7.18.3 + pac-proxy-agent: 7.2.0 + proxy-from-env: 1.1.0 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + + proxy-from-env@1.1.0: {} + + qs@6.15.0: + dependencies: + side-channel: 1.1.0 + + react-dom@19.2.4(react@19.2.4): + dependencies: + react: 19.2.4 + scheduler: 0.27.0 + + react-refresh@0.18.0: {} + + react-router-dom@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-router: 7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + + react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + cookie: 1.1.1 + react: 19.2.4 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 19.2.4(react@19.2.4) + + react@19.2.4: {} + + read@1.0.7: + dependencies: + mute-stream: 0.0.8 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + require-in-the-middle@5.2.0: + dependencies: + debug: 4.4.3 + module-details-from-path: 1.0.4 + resolve: 1.22.11 + transitivePeerDependencies: + - supports-color + + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + retry@0.13.1: {} + + rimraf@6.1.3: + dependencies: + glob: 13.0.4 + package-json-from-dist: 1.0.1 + + run-series@1.1.9: {} + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + sax@1.4.4: {} + + scheduler@0.27.0: {} + + semver@7.5.4: + dependencies: + lru-cache: 6.0.0 + + semver@7.7.2: {} + + set-cookie-parser@2.7.2: {} + + shimmer@1.2.1: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@3.0.7: {} + + smart-buffer@4.2.0: {} + + socks-proxy-agent@8.0.5: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + socks: 2.8.7 + transitivePeerDependencies: + - supports-color + + socks@2.8.7: + dependencies: + ip-address: 10.1.0 + smart-buffer: 4.2.0 + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + sprintf-js@1.1.2: {} + + stackframe@1.3.4: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + systeminformation@5.31.0: + optional: true + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tslib@1.9.3: {} + + tslib@2.8.1: {} + + tv4@1.3.0: {} + + tx2@1.0.5: + dependencies: + json-stringify-safe: 5.0.1 + optional: true + + type-fest@4.41.0: {} + + typescript@5.9.3: {} + + undici-types@7.16.0: {} + + vizion@2.2.1: + dependencies: + async: 2.6.4 + git-node-fs: 1.0.0(js-git@0.7.8) + ini: 1.3.8 + js-git: 0.7.8 + + ws@7.5.10: {} + + yallist@4.0.0: {} + + zod@4.3.6: {} diff --git a/implementations/web-react/rsbuild.config.ts b/implementations/web-react/rsbuild.config.ts new file mode 100644 index 00000000..f301a22c --- /dev/null +++ b/implementations/web-react/rsbuild.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from '@rsbuild/core' +import { pluginReact } from '@rsbuild/plugin-react' + +export default defineConfig({ + plugins: [pluginReact()], + source: { + entry: { + index: './src/main.tsx', + }, + }, + html: { + template: './index.html', + }, + output: { + target: 'web', + distPath: { + root: 'dist', + }, + }, + server: { + port: 3001, + }, +}) diff --git a/implementations/web-react/src/App.tsx b/implementations/web-react/src/App.tsx new file mode 100644 index 00000000..f25cabb2 --- /dev/null +++ b/implementations/web-react/src/App.tsx @@ -0,0 +1,158 @@ +import { type JSX, useEffect, useMemo, useState } from 'react' +import { Link, Navigate, Route, Routes, useLocation } from 'react-router-dom' +import { AnalyticsEventDisplay } from './components/AnalyticsEventDisplay' +import { ENTRY_IDS } from './config/entries' +import { HOME_PATH, PAGE_TWO_PATH } from './config/routes' +import { useOptimization } from './optimization/hooks/useOptimization' +import { useOptimizationState } from './optimization/hooks/useOptimizationState' +import { HomePage } from './pages/HomePage' +import { PageTwoPage } from './pages/PageTwoPage' +import { fetchEntries, getContentfulConfigError } from './services/contentfulClient' +import type { ContentfulEntry } from './types/contentful' + +function isIdentifiedProfile(profile: unknown): boolean { + if (typeof profile !== 'object' || profile === null) { + return false + } + + const record = profile as { traits?: unknown } + if (typeof record.traits !== 'object' || record.traits === null) { + return false + } + + const traits = record.traits as { identified?: unknown } + return Boolean(traits.identified) +} + +function hasEntries(entries: ContentfulEntry[]): boolean { + return entries.length > 0 +} + +function toEntryMap(entries: ContentfulEntry[]): Map { + return new Map(entries.map((entry) => [entry.sys.id, entry])) +} + +export default function App(): JSX.Element { + const location = useLocation() + const { sdk, isReady, error } = useOptimization() + const { consent, profile, personalizations } = useOptimizationState(sdk?.states) + + const [entries, setEntries] = useState([]) + const [entriesError, setEntriesError] = useState(null) + + useEffect(() => { + if (!isReady || sdk === undefined) { + return + } + + void sdk.personalization.page({ properties: { url: location.pathname } }) + }, [isReady, location.pathname, sdk]) + + useEffect(() => { + if (!isReady || sdk === undefined) { + return + } + + const configError = getContentfulConfigError() + if (configError) { + setEntriesError(configError) + return + } + + void fetchEntries(ENTRY_IDS) + .then((nextEntries) => { + setEntries(nextEntries) + setEntriesError( + nextEntries.length === 0 + ? 'No entries were loaded. Verify mock server and Contentful env configuration.' + : null, + ) + }) + .catch((fetchError: unknown) => { + const message = + fetchError instanceof Error ? fetchError.message : 'Unknown entry load error' + setEntriesError(message) + }) + }, [isReady, sdk]) + + const isIdentified = useMemo(() => isIdentifiedProfile(profile), [profile]) + const entriesById = useMemo(() => toEntryMap(entries), [entries]) + const personalizationCount = useMemo( + () => (Array.isArray(personalizations) ? personalizations.length : 0), + [personalizations], + ) + + const handleIdentify = (): void => { + if (!isReady || sdk === undefined) { + return + } + + void sdk.personalization.identify({ userId: 'charles', traits: { identified: true } }) + } + + const handleReset = (): void => { + if (!isReady || sdk === undefined) { + return + } + + sdk.reset() + } + + const handleConsent = (accepted: boolean): void => { + if (!isReady || sdk === undefined) { + return + } + + sdk.consent(accepted) + } + + if (error) { + return

{error.message}

+ } + + if (!isReady || sdk === undefined) { + return

Loading SDK...

+ } + + if (entriesError) { + return

{entriesError}

+ } + + if (!hasEntries(entries)) { + return

Loading entries...

+ } + + return ( +
+ + + + + } + /> + } /> + } /> + + + +
+ ) +} diff --git a/implementations/web-react/src/components/AnalyticsEventDisplay.tsx b/implementations/web-react/src/components/AnalyticsEventDisplay.tsx new file mode 100644 index 00000000..574afa63 --- /dev/null +++ b/implementations/web-react/src/components/AnalyticsEventDisplay.tsx @@ -0,0 +1,74 @@ +import { type JSX, useEffect, useRef, useState } from 'react' +import { useOptimization } from '../optimization/hooks/useOptimization' +import { isRecord } from '../utils/typeGuards' + +interface AnalyticsEvent { + id: string + componentId?: string + type: string +} + +function toAnalyticsEvent(event: unknown, id: string): AnalyticsEvent | undefined { + if (!isRecord(event) || typeof event.type !== 'string') { + return undefined + } + + const componentId = typeof event.componentId === 'string' ? event.componentId : undefined + + return { + id, + componentId, + type: event.type, + } +} + +export function AnalyticsEventDisplay(): JSX.Element { + const { sdk, isReady } = useOptimization() + const [events, setEvents] = useState([]) + const nextId = useRef(0) + + useEffect(() => { + if (!isReady || sdk === undefined) { + setEvents([]) + return + } + + const subscription = sdk.states.eventStream.subscribe((event: unknown) => { + const id = `event-${nextId.current}` + nextId.current += 1 + const nextEvent = toAnalyticsEvent(event, id) + if (!nextEvent) { + return + } + + setEvents((previous) => [nextEvent, ...previous]) + }) + + return () => { + subscription.unsubscribe() + } + }, [isReady, sdk]) + + return ( +
+

Analytics Events

+

Events: {events.length}

+ {events.length === 0 ?

No events tracked yet

: null} + +
    + {events.map((event) => { + const testId = event.componentId + ? `event-${event.type}-${event.componentId}` + : `event-${event.type}-${event.id}` + + return ( +
  • + {event.type} + {event.componentId ? ` - Component: ${event.componentId}` : ''} +
  • + ) + })} +
+
+ ) +} diff --git a/implementations/web-react/src/components/RichTextRenderer.tsx b/implementations/web-react/src/components/RichTextRenderer.tsx new file mode 100644 index 00000000..70ab37b3 --- /dev/null +++ b/implementations/web-react/src/components/RichTextRenderer.tsx @@ -0,0 +1,102 @@ +import { documentToReactComponents, type Options } from '@contentful/rich-text-react-renderer' +import { INLINES } from '@contentful/rich-text-types' +import type { JSX } from 'react' +import { + usePersonalization, + type UsePersonalizationResult, +} from '../optimization/hooks/usePersonalization' +import type { RichTextDocument } from '../types/contentful' +import { isRecord } from '../utils/typeGuards' + +interface RichTextNode { + nodeType: string + content?: RichTextNode[] + data?: Record + value?: string +} + +interface RichTextRendererProps { + richText: RichTextDocument +} + +type MergeTagValueResolver = UsePersonalizationResult['getMergeTagValue'] +type MergeTagEntry = Parameters[0] +const EMBEDDED_ENTRY_NODE_TYPE = 'embedded-entry-inline' + +function isLink(target: unknown): target is { sys: { type: 'Link' } } { + if (!isRecord(target) || !isRecord(target.sys)) { + return false + } + + return target.sys.type === 'Link' +} + +function isMergeTagEntry(entry: unknown): entry is MergeTagEntry { + if (!isRecord(entry)) { + return false + } + + const { sys } = entry + if (!isRecord(sys) || !isRecord(sys.contentType)) { + return false + } + + const { contentType } = sys + if (!isRecord(contentType) || !isRecord(contentType.sys)) { + return false + } + + return contentType.sys.id === 'nt_mergetag' +} + +function getMergeTagText(target: unknown, getMergeTagValue: MergeTagValueResolver): string { + if (isLink(target) || !isMergeTagEntry(target)) { + return '[Merge Tag]' + } + + return getMergeTagValue(target) +} + +function extractTextContent(node: RichTextNode, getMergeTagValue: MergeTagValueResolver): string { + if (node.nodeType === 'text' && typeof node.value === 'string') { + return node.value + } + + if (node.nodeType === EMBEDDED_ENTRY_NODE_TYPE && isRecord(node.data) && 'target' in node.data) { + return getMergeTagText(node.data.target, getMergeTagValue) + } + + if (Array.isArray(node.content)) { + return node.content.map((child) => extractTextContent(child, getMergeTagValue)).join(' ') + } + + return '' +} + +export function getRichTextContent( + richText: RichTextDocument, + getMergeTagValue: MergeTagValueResolver, +): string { + return richText.content + .map((node) => extractTextContent(node, getMergeTagValue)) + .join(' ') + .trim() +} + +export function RichTextRenderer({ richText }: RichTextRendererProps): JSX.Element { + const { getMergeTagValue } = usePersonalization() + const renderOptions: Options = { + renderNode: { + [INLINES.EMBEDDED_ENTRY]: (node): string => { + const { data } = node + if (!isRecord(data) || !('target' in data)) { + return '[Merge Tag]' + } + + return getMergeTagText(data.target, getMergeTagValue) + }, + }, + } + + return <>{documentToReactComponents(richText, renderOptions)} +} diff --git a/implementations/web-react/src/config/entries.ts b/implementations/web-react/src/config/entries.ts new file mode 100644 index 00000000..f65df260 --- /dev/null +++ b/implementations/web-react/src/config/entries.ts @@ -0,0 +1,15 @@ +export const AUTO_OBSERVED_ENTRY_IDS = [ + '1JAU028vQ7v6nB2swl3NBo', + '1MwiFl4z7gkwqGYdvCmr8c', + '4ib0hsHWoSOnCVdDkizE8d', + 'xFwgG3oNaOcjzWiGe4vXo', + '2Z2WLOx07InSewC3LUB3eX', +] as const + +export const MANUALLY_OBSERVED_ENTRY_IDS = [ + '5XHssysWUDECHzKLzoIsg1', + '6zqoWXyiSrf0ja7I2WGtYj', + '7pa5bOx8Z9NmNcr7mISvD', +] as const + +export const ENTRY_IDS = [...AUTO_OBSERVED_ENTRY_IDS, ...MANUALLY_OBSERVED_ENTRY_IDS] as const diff --git a/implementations/web-react/src/config/routes.ts b/implementations/web-react/src/config/routes.ts new file mode 100644 index 00000000..27c95c63 --- /dev/null +++ b/implementations/web-react/src/config/routes.ts @@ -0,0 +1,2 @@ +export const HOME_PATH = '/' +export const PAGE_TWO_PATH = '/page-two' diff --git a/implementations/web-react/src/main.tsx b/implementations/web-react/src/main.tsx new file mode 100644 index 00000000..e54713e1 --- /dev/null +++ b/implementations/web-react/src/main.tsx @@ -0,0 +1,25 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import { BrowserRouter } from 'react-router-dom' +import App from './App' +import { OptimizationProvider } from './optimization/OptimizationProvider' + +function main(): void { + const rootElement = document.getElementById('root') + + if (!rootElement) { + throw new Error('Root element not found') + } + + ReactDOM.createRoot(rootElement).render( + + + + + + + , + ) +} + +main() diff --git a/implementations/web-react/src/optimization/OptimizationProvider.tsx b/implementations/web-react/src/optimization/OptimizationProvider.tsx new file mode 100644 index 00000000..435a09e1 --- /dev/null +++ b/implementations/web-react/src/optimization/OptimizationProvider.tsx @@ -0,0 +1,30 @@ +import { createContext, type JSX, type PropsWithChildren, useMemo } from 'react' +import { getOptimization, type OptimizationInstance } from './createOptimization' + +export interface OptimizationContextValue { + sdk: OptimizationInstance | undefined + isReady: boolean + error: Error | undefined +} + +export const OptimizationContext = createContext(undefined) + +export function OptimizationProvider({ children }: PropsWithChildren): JSX.Element { + const value = useMemo(() => { + try { + // Intentionally use a singleton SDK instance to avoid re-initialization during + // React StrictMode double invocation in development. + const sdk = getOptimization() + return { sdk, isReady: true, error: undefined } + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown Optimization init error' + return { + sdk: undefined, + isReady: false, + error: error instanceof Error ? error : new Error(message), + } + } + }, []) + + return {children} +} diff --git a/implementations/web-react/src/optimization/createOptimization.ts b/implementations/web-react/src/optimization/createOptimization.ts new file mode 100644 index 00000000..c8def1c1 --- /dev/null +++ b/implementations/web-react/src/optimization/createOptimization.ts @@ -0,0 +1,81 @@ +import Optimization from '@contentful/optimization-web' + +export type OptimizationInstance = Optimization +export type OptimizationConfig = ConstructorParameters[0] + +const OPTIMIZATION_CLIENT_ID = + import.meta.env.PUBLIC_NINETAILED_CLIENT_ID?.trim() ?? 'mock-client-id' +const OPTIMIZATION_ENVIRONMENT = import.meta.env.PUBLIC_NINETAILED_ENVIRONMENT?.trim() ?? 'main' +const EXPERIENCE_API_BASE_URL = + import.meta.env.PUBLIC_EXPERIENCE_API_BASE_URL?.trim() ?? 'http://localhost:8000/experience/' +const INSIGHTS_API_BASE_URL = + import.meta.env.PUBLIC_INSIGHTS_API_BASE_URL?.trim() ?? 'http://localhost:8000/insights/' +const OPTIMIZATION_LOG_LEVEL = import.meta.env.PUBLIC_OPTIMIZATION_LOG_LEVEL?.trim().toLowerCase() +type OptimizationLogLevel = 'debug' | 'warn' | 'error' + +function resolveLogLevel(): OptimizationLogLevel { + if (OPTIMIZATION_LOG_LEVEL === 'debug') { + return 'debug' + } + + if (OPTIMIZATION_LOG_LEVEL === 'warn') { + return 'warn' + } + + if (OPTIMIZATION_LOG_LEVEL === 'error') { + return 'error' + } + + return import.meta.env.DEV ? 'debug' : 'warn' +} + +class OptimizationInitializationError extends Error { + public readonly cause: unknown + + public constructor(message: string, cause: unknown) { + super(message) + this.name = 'OptimizationInitializationError' + this.cause = cause + } +} + +function createOptimizationConfig(): OptimizationConfig { + return { + clientId: OPTIMIZATION_CLIENT_ID, + environment: OPTIMIZATION_ENVIRONMENT, + logLevel: resolveLogLevel(), + autoTrackEntryViews: true, + app: { + name: 'Optimization SDK - React Web Reference', + version: '0.1.0', + }, + analytics: { + baseUrl: INSIGHTS_API_BASE_URL, + }, + personalization: { + baseUrl: EXPERIENCE_API_BASE_URL, + }, + } +} + +export function createOptimization(): OptimizationInstance { + try { + const config = createOptimizationConfig() + return new Optimization(config) + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown Optimization init error' + throw new OptimizationInitializationError( + `Failed to initialize Optimization SDK: ${message}`, + error, + ) + } +} + +let optimizationInstance: OptimizationInstance | undefined = undefined + +export function getOptimization(): OptimizationInstance { + // Keep a single process-wide instance for this reference implementation. + optimizationInstance ??= createOptimization() + + return optimizationInstance +} diff --git a/implementations/web-react/src/optimization/hooks/useAnalytics.ts b/implementations/web-react/src/optimization/hooks/useAnalytics.ts new file mode 100644 index 00000000..3b263a1d --- /dev/null +++ b/implementations/web-react/src/optimization/hooks/useAnalytics.ts @@ -0,0 +1,27 @@ +import type Optimization from '@contentful/optimization-web' +import { useMemo } from 'react' +import { useOptimization } from './useOptimization' + +type TrackComponentViewPayload = Parameters[0] +type TrackComponentViewResult = ReturnType + +export interface UseAnalyticsResult { + trackView: (payload: TrackComponentViewPayload) => TrackComponentViewResult | undefined +} + +export function useAnalytics(): UseAnalyticsResult { + const { sdk, isReady } = useOptimization() + + return useMemo(() => { + if (!isReady || sdk === undefined) { + return { + trackView: (_payload: TrackComponentViewPayload): undefined => undefined, + } + } + + return { + trackView: async (payload: TrackComponentViewPayload): TrackComponentViewResult => + await sdk.trackComponentView(payload), + } + }, [isReady, sdk]) +} diff --git a/implementations/web-react/src/optimization/hooks/useOptimization.ts b/implementations/web-react/src/optimization/hooks/useOptimization.ts new file mode 100644 index 00000000..1678eaf3 --- /dev/null +++ b/implementations/web-react/src/optimization/hooks/useOptimization.ts @@ -0,0 +1,15 @@ +import { useContext } from 'react' +import { OptimizationContext, type OptimizationContextValue } from '../OptimizationProvider' + +function assertContext(value: OptimizationContextValue | undefined): OptimizationContextValue { + if (value === undefined) { + throw new Error('useOptimization must be used within an OptimizationProvider') + } + + return value +} + +export function useOptimization(): OptimizationContextValue { + const context = useContext(OptimizationContext) + return assertContext(context) +} diff --git a/implementations/web-react/src/optimization/hooks/useOptimizationState.ts b/implementations/web-react/src/optimization/hooks/useOptimizationState.ts new file mode 100644 index 00000000..1ae03dfa --- /dev/null +++ b/implementations/web-react/src/optimization/hooks/useOptimizationState.ts @@ -0,0 +1,60 @@ +import type { CoreStates } from '@contentful/optimization-web' +import { useEffect, useState } from 'react' + +interface Subscription { + unsubscribe: () => void +} + +interface Observable { + subscribe: (next: (value: TValue) => void) => Subscription +} + +type StateValue = Parameters< + Parameters[0] +>[0] + +export interface OptimizationStateSnapshot { + consent: boolean | undefined + eventStream: StateValue<'eventStream'> | undefined + flags: StateValue<'flags'> | undefined + personalizations: StateValue<'personalizations'> | undefined + profile: StateValue<'profile'> | undefined +} + +function useObservableState( + observable: Observable | undefined, +): TValue | undefined { + const [value, setValue] = useState(undefined) + + useEffect(() => { + if (!observable) { + setValue(undefined) + return + } + + const subscription = observable.subscribe((nextValue: TValue) => { + setValue(nextValue) + }) + + return () => { + subscription.unsubscribe() + } + }, [observable]) + return value +} + +export function useOptimizationState(sdk: CoreStates | undefined): OptimizationStateSnapshot { + const consent = useObservableState(sdk?.consent) + const eventStream = useObservableState(sdk?.eventStream) + const flags = useObservableState(sdk?.flags) + const personalizations = useObservableState(sdk?.personalizations) + const profile = useObservableState(sdk?.profile) + + return { + consent, + eventStream, + flags, + personalizations, + profile, + } +} diff --git a/implementations/web-react/src/optimization/hooks/usePersonalization.ts b/implementations/web-react/src/optimization/hooks/usePersonalization.ts new file mode 100644 index 00000000..77856344 --- /dev/null +++ b/implementations/web-react/src/optimization/hooks/usePersonalization.ts @@ -0,0 +1,58 @@ +import type Optimization from '@contentful/optimization-web' +import { useMemo } from 'react' +import { useOptimization } from './useOptimization' + +type PersonalizationApi = Optimization['personalization'] +type BaselineEntry = Parameters[0] +type PersonalizeResult = ReturnType +type MergeTagTarget = Parameters[0] + +export interface UsePersonalizationResult { + resolveEntry: (baselineEntry: BaselineEntry) => PersonalizeResult + getMergeTagValue: (mergeTagEntry: MergeTagTarget) => string +} + +function fallbackResolveEntry(baselineEntry: BaselineEntry): PersonalizeResult { + return { entry: baselineEntry } +} + +function toStringValue(value: unknown): string { + if (value === undefined || value === null) { + return '' + } + + if (typeof value === 'string') { + return value + } + + if (typeof value === 'number' || typeof value === 'bigint' || typeof value === 'boolean') { + return `${value}` + } + + if (typeof value === 'symbol') { + return value.description ?? value.toString() + } + + return JSON.stringify(value) +} + +export function usePersonalization(): UsePersonalizationResult { + const { sdk, isReady } = useOptimization() + + return useMemo(() => { + if (!isReady || sdk === undefined) { + return { + resolveEntry: fallbackResolveEntry, + getMergeTagValue: (_mergeTagEntry: MergeTagTarget): string => '', + } + } + + return { + resolveEntry: (baselineEntry: BaselineEntry): PersonalizeResult => + sdk.personalization.personalizeEntry(baselineEntry), + + getMergeTagValue: (mergeTagEntry: MergeTagTarget): string => + toStringValue(sdk.personalization.getMergeTagValue(mergeTagEntry)), + } + }, [isReady, sdk]) +} diff --git a/implementations/web-react/src/pages/HomePage.tsx b/implementations/web-react/src/pages/HomePage.tsx new file mode 100644 index 00000000..72e3b995 --- /dev/null +++ b/implementations/web-react/src/pages/HomePage.tsx @@ -0,0 +1,106 @@ +import type { JSX } from 'react' +import { AUTO_OBSERVED_ENTRY_IDS, MANUALLY_OBSERVED_ENTRY_IDS } from '../config/entries' +import { ContentEntry } from '../sections/ContentEntry' +import { NestedContentEntry } from '../sections/NestedContentEntry' +import type { ContentfulEntry } from '../types/contentful' + +interface HomePageProps { + consent: boolean | undefined + entriesById: Map + isIdentified: boolean + personalizationCount: number + onConsentChange: (accepted: boolean) => void + onIdentify: () => void + onReset: () => void +} + +function isConsentAccepted(consent: boolean | undefined): boolean { + return consent === true +} + +export function HomePage({ + consent, + entriesById, + isIdentified, + personalizationCount, + onConsentChange, + onIdentify, + onReset, +}: HomePageProps): JSX.Element { + return ( + <> +
+

Utilities

+ +
+ {isConsentAccepted(consent) ? ( + + ) : ( + + )} + + {!isIdentified ? ( + + ) : ( + + )} +
+ +

Consent: {String(consent)}

+

Personalizations: {personalizationCount}

+
+ +
+

Auto Observed Entries

+
+ {AUTO_OBSERVED_ENTRY_IDS.map((entryId) => { + const entry = entriesById.get(entryId) + if (!entry) { + return null + } + + if (entry.sys.contentType.sys.id === 'nestedContent') { + return + } + + return + })} +
+
+ +
+

Manually Observed Entries

+
+ {MANUALLY_OBSERVED_ENTRY_IDS.map((entryId) => { + const entry = entriesById.get(entryId) + if (!entry) { + return null + } + + return + })} +
+
+ + ) +} diff --git a/implementations/web-react/src/pages/PageTwoPage.tsx b/implementations/web-react/src/pages/PageTwoPage.tsx new file mode 100644 index 00000000..9410c299 --- /dev/null +++ b/implementations/web-react/src/pages/PageTwoPage.tsx @@ -0,0 +1,15 @@ +import type { JSX } from 'react' +import { Link } from 'react-router-dom' +import { HOME_PATH } from '../config/routes' + +export function PageTwoPage(): JSX.Element { + return ( +
+

Page Two

+

Secondary route for SPA navigation and page event validation.

+ + Back to Home + +
+ ) +} diff --git a/implementations/web-react/src/sections/ContentEntry.tsx b/implementations/web-react/src/sections/ContentEntry.tsx new file mode 100644 index 00000000..8012d931 --- /dev/null +++ b/implementations/web-react/src/sections/ContentEntry.tsx @@ -0,0 +1,126 @@ +import { type JSX, useEffect, useMemo, useRef } from 'react' +import { RichTextRenderer } from '../components/RichTextRenderer' +import { useOptimization } from '../optimization/hooks/useOptimization' +import { usePersonalization } from '../optimization/hooks/usePersonalization' +import type { ContentfulEntry, RichTextDocument } from '../types/contentful' +import { isRecord } from '../utils/typeGuards' + +interface ContentEntryProps { + entry: ContentfulEntry + observation: 'auto' | 'manual' +} + +interface PersonalizationMeta { + experienceId?: string + sticky?: boolean + variantIndex?: number +} + +function isRichTextField(field: unknown): field is RichTextDocument { + return ( + typeof field === 'object' && + field !== null && + 'nodeType' in field && + (field as { nodeType: unknown }).nodeType === 'document' && + 'content' in field && + Array.isArray((field as { content: unknown }).content) + ) +} + +function getPersonalizationMeta(value: unknown): PersonalizationMeta { + if (!isRecord(value)) { + return {} + } + + const experienceId = typeof value.experienceId === 'string' ? value.experienceId : undefined + const sticky = typeof value.sticky === 'boolean' ? value.sticky : undefined + const variantIndex = typeof value.variantIndex === 'number' ? value.variantIndex : undefined + + return { experienceId, sticky, variantIndex } +} + +function getEntryText(contentEntry: ContentfulEntry): string { + return typeof contentEntry.fields.text === 'string' ? contentEntry.fields.text : 'No content' +} + +export function ContentEntry({ entry, observation }: ContentEntryProps): JSX.Element { + const { sdk, isReady } = useOptimization() + const { resolveEntry } = usePersonalization() + const containerRef = useRef(null) + + const resolved = useMemo(() => resolveEntry(entry), [entry, resolveEntry]) + const { entry: resolvedEntry, personalization } = resolved + + const { experienceId, sticky, variantIndex } = useMemo( + () => getPersonalizationMeta(personalization), + [personalization], + ) + + useEffect(() => { + if (!isReady || sdk === undefined || observation !== 'manual') { + return + } + + const { current: element } = containerRef + if (!element) { + return + } + + sdk.untrackEntryViewForElement(element) + + sdk.trackEntryViewForElement(element, { + data: { + entryId: resolvedEntry.sys.id, + personalizationId: experienceId, + sticky, + variantIndex, + }, + }) + + return () => { + sdk.untrackEntryViewForElement(element) + } + }, [experienceId, isReady, observation, resolvedEntry.sys.id, sdk, sticky, variantIndex]) + + const richTextField = Object.values(resolvedEntry.fields).find(isRichTextField) + + const fullLabel = `Entry: ${resolvedEntry.sys.id}` + + const autoTrackingAttributes = + observation === 'auto' + ? { + 'data-ctfl-entry-id': resolvedEntry.sys.id, + 'data-ctfl-baseline-id': entry.sys.id, + 'data-ctfl-personalization-id': experienceId, + 'data-ctfl-sticky': sticky === undefined ? undefined : String(sticky), + 'data-ctfl-variant-index': variantIndex === undefined ? undefined : String(variantIndex), + } + : undefined + + const manualTrackingAttributes = + observation === 'manual' + ? { + 'data-entry-id': entry.sys.id, + } + : undefined + + return ( +
+
+
+ {richTextField ? ( + + ) : ( +

{getEntryText(resolvedEntry)}

+ )} +

{`[Entry: ${entry.sys.id}]`}

+
+
+
+ ) +} diff --git a/implementations/web-react/src/sections/NestedContentEntry.tsx b/implementations/web-react/src/sections/NestedContentEntry.tsx new file mode 100644 index 00000000..32733f11 --- /dev/null +++ b/implementations/web-react/src/sections/NestedContentEntry.tsx @@ -0,0 +1,15 @@ +import type { JSX } from 'react' +import type { ContentfulEntry } from '../types/contentful' +import { NestedContentItem } from './NestedContentItem' + +interface NestedContentEntryProps { + entry: ContentfulEntry +} + +export function NestedContentEntry({ entry }: NestedContentEntryProps): JSX.Element { + return ( +
+ +
+ ) +} diff --git a/implementations/web-react/src/sections/NestedContentItem.tsx b/implementations/web-react/src/sections/NestedContentItem.tsx new file mode 100644 index 00000000..bdd4e95d --- /dev/null +++ b/implementations/web-react/src/sections/NestedContentItem.tsx @@ -0,0 +1,76 @@ +import { useMemo, type JSX } from 'react' +import { usePersonalization } from '../optimization/hooks/usePersonalization' +import type { ContentfulEntry } from '../types/contentful' +import { isRecord } from '../utils/typeGuards' + +interface NestedContentItemProps { + entry: ContentfulEntry +} + +interface PersonalizationMeta { + experienceId?: string + sticky?: boolean + variantIndex?: number +} + +function isEntry(value: unknown): value is ContentfulEntry { + return ( + isRecord(value) && + isRecord(value.sys) && + typeof value.sys.id === 'string' && + isRecord(value.fields) + ) +} + +function getPersonalizationMeta(value: unknown): PersonalizationMeta { + if (!isRecord(value)) { + return {} + } + + return { + experienceId: typeof value.experienceId === 'string' ? value.experienceId : undefined, + sticky: typeof value.sticky === 'boolean' ? value.sticky : undefined, + variantIndex: typeof value.variantIndex === 'number' ? value.variantIndex : undefined, + } +} + +function renderText(contentEntry: ContentfulEntry): string { + return typeof contentEntry.fields.text === 'string' ? contentEntry.fields.text : '' +} + +export function NestedContentItem({ entry }: NestedContentItemProps): JSX.Element { + const { resolveEntry } = usePersonalization() + const resolved = useMemo(() => resolveEntry(entry), [entry, resolveEntry]) + const { entry: resolvedEntry, personalization } = resolved + + const { experienceId, sticky, variantIndex } = useMemo( + () => getPersonalizationMeta(personalization), + [personalization], + ) + + const nestedEntries = Array.isArray(resolvedEntry.fields.nested) + ? resolvedEntry.fields.nested + : [] + + const text = renderText(resolvedEntry) + const fullLabel = `${text} [Entry: ${resolvedEntry.sys.id}]` + + return ( +
+
+

{text}

+

{`[Entry: ${resolvedEntry.sys.id}]`}

+
+ + {nestedEntries.filter(isEntry).map((nestedEntry) => ( + + ))} +
+ ) +} diff --git a/implementations/web-react/src/services/contentfulClient.ts b/implementations/web-react/src/services/contentfulClient.ts new file mode 100644 index 00000000..e25fb483 --- /dev/null +++ b/implementations/web-react/src/services/contentfulClient.ts @@ -0,0 +1,59 @@ +import { createClient } from 'contentful' +import type { ContentEntrySkeleton, ContentfulEntry } from '../types/contentful' + +const INCLUDE_DEPTH = 10 +const CONTENTFUL_ACCESS_TOKEN = import.meta.env.PUBLIC_CONTENTFUL_TOKEN?.trim() ?? '' +const CONTENTFUL_BASE_PATH = import.meta.env.PUBLIC_CONTENTFUL_BASE_PATH?.trim() +const CONTENTFUL_ENVIRONMENT = import.meta.env.PUBLIC_CONTENTFUL_ENVIRONMENT?.trim() ?? '' +const CONTENTFUL_HOST = import.meta.env.PUBLIC_CONTENTFUL_CDA_HOST?.trim() ?? '' +const CONTENTFUL_SPACE_ID = import.meta.env.PUBLIC_CONTENTFUL_SPACE_ID?.trim() ?? '' +const MISSING_ENV_ERROR = [ + ['PUBLIC_CONTENTFUL_TOKEN', CONTENTFUL_ACCESS_TOKEN], + ['PUBLIC_CONTENTFUL_ENVIRONMENT', CONTENTFUL_ENVIRONMENT], + ['PUBLIC_CONTENTFUL_SPACE_ID', CONTENTFUL_SPACE_ID], + ['PUBLIC_CONTENTFUL_CDA_HOST', CONTENTFUL_HOST], +] + .filter(([, value]) => value.length === 0) + .map(([key]) => key) + .join(', ') + +function createContentfulClient(): ReturnType { + return createClient({ + accessToken: CONTENTFUL_ACCESS_TOKEN, + environment: CONTENTFUL_ENVIRONMENT, + host: CONTENTFUL_HOST, + insecure: CONTENTFUL_HOST.includes('localhost'), + space: CONTENTFUL_SPACE_ID, + ...(CONTENTFUL_BASE_PATH ? { basePath: CONTENTFUL_BASE_PATH } : {}), + }) +} + +const contentfulClient = createContentfulClient() + +export function getContentfulConfigError(): string | null { + if (MISSING_ENV_ERROR.length === 0) { + return null + } + + return `Missing required Contentful env vars: ${MISSING_ENV_ERROR}. See implementations/web-react/.env.example.` +} + +export async function fetchEntry(entryId: string): Promise { + if (getContentfulConfigError()) { + return undefined + } + + try { + return await contentfulClient.getEntry(entryId, { + include: INCLUDE_DEPTH, + }) + } catch { + return undefined + } +} + +export async function fetchEntries(entryIds: readonly string[]): Promise { + const fetchedEntries = await Promise.all(entryIds.map(fetchEntry)) + + return fetchedEntries.filter((entry): entry is ContentfulEntry => entry !== undefined) +} diff --git a/implementations/web-react/src/types/contentful.ts b/implementations/web-react/src/types/contentful.ts new file mode 100644 index 00000000..67924a9c --- /dev/null +++ b/implementations/web-react/src/types/contentful.ts @@ -0,0 +1,11 @@ +import type { Document } from '@contentful/rich-text-types' +import type { Entry, EntryFieldTypes, EntrySkeletonType } from 'contentful' + +export interface ContentEntryFields { + text?: EntryFieldTypes.Text | EntryFieldTypes.RichText + nested?: EntryFieldTypes.Array> +} + +export type ContentEntrySkeleton = EntrySkeletonType +export type ContentfulEntry = Entry +export type RichTextDocument = Document diff --git a/implementations/web-react/src/types/env.d.ts b/implementations/web-react/src/types/env.d.ts new file mode 100644 index 00000000..573a3cf6 --- /dev/null +++ b/implementations/web-react/src/types/env.d.ts @@ -0,0 +1,17 @@ +interface ImportMetaEnv { + readonly DEV: boolean + readonly PUBLIC_CONTENTFUL_BASE_PATH?: string + readonly PUBLIC_CONTENTFUL_CDA_HOST?: string + readonly PUBLIC_CONTENTFUL_ENVIRONMENT?: string + readonly PUBLIC_CONTENTFUL_SPACE_ID?: string + readonly PUBLIC_CONTENTFUL_TOKEN?: string + readonly PUBLIC_OPTIMIZATION_LOG_LEVEL?: 'debug' | 'warn' | 'error' + readonly PUBLIC_EXPERIENCE_API_BASE_URL?: string + readonly PUBLIC_INSIGHTS_API_BASE_URL?: string + readonly PUBLIC_NINETAILED_CLIENT_ID?: string + readonly PUBLIC_NINETAILED_ENVIRONMENT?: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} diff --git a/implementations/web-react/src/utils/typeGuards.ts b/implementations/web-react/src/utils/typeGuards.ts new file mode 100644 index 00000000..533bad5e --- /dev/null +++ b/implementations/web-react/src/utils/typeGuards.ts @@ -0,0 +1,3 @@ +export function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null +} diff --git a/implementations/web-react/tsconfig.json b/implementations/web-react/tsconfig.json new file mode 100644 index 00000000..2f183201 --- /dev/null +++ b/implementations/web-react/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src"] +} diff --git a/package.json b/package.json index 723cb7b5..6a0adc18 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "implementation:node-ssr-only": "pnpm run implementation:run -- node-ssr-only", "implementation:node-ssr-web-vanilla": "pnpm run implementation:run -- node-ssr-web-vanilla", "implementation:react-native": "pnpm run implementation:run -- react-native", + "implementation:web-react": "pnpm run implementation:run -- web-react", "implementation:install": "pnpm run implementation:run -- --all -- implementation:install", "implementation:run": "tsx ./scripts/run-implementation-script.ts", "implementation:web-vanilla": "pnpm run implementation:run -- web-vanilla",