diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..35e2d8f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [20, 22] + + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm test diff --git a/.gitignore b/.gitignore index 22de42a..0b6561d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ node_modules/ .env dist/ coverage/ +.next/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3be94c0 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,40 @@ +# Changelog + +All notable changes to this project are documented in this file. + +## [1.1.0] - 2026-06-20 + +### Added + +- `theme` prop on `CookieConsent` for brand color customization via CSS variables +- `className` prop on `CookieConsent` +- Optional `cookies` prop with expandable cookie inventory tables per category +- New label keys for cookie table headers and storage error messages +- `onConsentSaved` and `onConsentError` callbacks on `CookieConsent` +- `PersistResult` return values from `setConsent`, `acceptAll`, and `rejectAll` +- `configureConsent({ onStorageError })` global error callback +- `debug: true` console warnings for invalid or failed consent writes +- `ads_data_redaction` and `url_passthrough` in bootstrap default script +- Multi-tab consent sync via the `storage` event +- Keyboard support: `Escape` closes preferences or returns to banner +- Focus management when the banner opens +- TypeScript definitions for `@pdntechnology/cookie-consent/react` +- Cookie schema helpers exported from the main package +- Vitest test suite and GitHub Actions CI +- Next.js demo app in `examples/next-app` +- Turkish documentation in `README.tr.md` +- `CONTRIBUTING.md` + +### Changed + +- Banner styles now use CSS custom properties instead of hardcoded colors +- Bootstrap script includes advanced Consent Mode settings by default + +## [1.0.0] - 2026-06-20 + +### Added + +- Initial release with Google Consent Mode v2 integration +- React banner and Next.js bootstrap components +- Optional GTM and Meta Pixel loaders +- Framework-agnostic consent API diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..8912478 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,72 @@ +# Contributing + +Thank you for considering a contribution to `@pdntechnology/cookie-consent`. + +## Before you start + +- Open an issue for large changes so we can agree on the approach first. +- Keep pull requests focused. One feature or fix per PR is easier to review. +- Match the existing code style: small modules, clear names, minimal dependencies. + +## Development setup + +```bash +git clone https://git.pdntechnology.com.tr/pdn/cookie-consent.git +cd cookie-consent +npm install +npm test +``` + +### Try the Next.js demo + +```bash +cd examples/next-app +npm install +npm run dev +``` + +The example uses a local file dependency on the parent package. + +## Tests + +All changes should pass the test suite: + +```bash +npm test +``` + +Add or update tests when you change behaviour in: + +- `src/preferences.js` +- `src/storage.js` +- `src/consent-api.js` +- `src/google/*` +- `src/cookie-schema.js` +- `src/react/theme.js` + +## Pull request checklist + +- [ ] `npm test` passes locally +- [ ] Public API changes are reflected in `src/index.d.ts` and/or `src/react/index.d.ts` +- [ ] User-facing changes are documented in `README.md` +- [ ] Turkish docs updated in `README.tr.md` when behaviour or public API changes +- [ ] `CHANGELOG.md` updated under `[Unreleased]` or the next version + +## Commit messages + +Use clear, imperative messages: + +- `Add theme prop to CookieConsent` +- `Fix bootstrap script advanced settings` +- `Document cookie inventory API` + +## Code guidelines + +- Prefer framework-agnostic logic in `src/` and React-specific UI in `src/react/` +- Do not add runtime dependencies without a strong reason +- Fail safely: never throw from public consent APIs in the browser +- Surface storage failures via return values, callbacks, or debug logging + +## License + +By contributing, you agree that your contributions will be licensed under the [MIT License](LICENSE). diff --git a/README.md b/README.md index f12d854..a0b44cd 100644 --- a/README.md +++ b/README.md @@ -29,12 +29,16 @@ This library implements that flow in ~15 KB of readable JavaScript. - Google Consent Mode v2 (all 7 signals) - Granular categories: Necessary / Analytics / Marketing -- `ads_data_redaction` + `url_passthrough` when marketing is denied +- Customizable banner text, theme colors, and cookie inventory +- `ads_data_redaction` + `url_passthrough` in bootstrap and on update - Regional default consent (EEA, etc.) - React banner + Next.js bootstrap component - Optional GTM and Meta Pixel loaders (consent-gated) - Framework-agnostic core API (vanilla JS supported) -- TypeScript definitions included +- TypeScript definitions for core API and React components +- Error callbacks when consent cannot be saved + +> Turkish documentation: [README.tr.md](README.tr.md) --- @@ -81,7 +85,7 @@ Import this file in your root layout **before** `ConsentBootstrap`. // app/layout.jsx import "@pdntechnology/cookie-consent/styles.css"; import "@/lib/consent-config"; -import ConsentBootstrap from "@pdntechnology/cookie-consent/react/ConsentBootstrap"; +import { ConsentBootstrap } from "@pdntechnology/cookie-consent/react"; export default function RootLayout({ children }) { return ( @@ -136,7 +140,142 @@ import { openPreferences } from "@pdntechnology/cookie-consent"; --- -## How categories map to Google signals +## Customization + +### Text and policy URL + +Every visible string can be overridden via the `labels` prop. The policy link uses `policyHref`. + +```jsx + +``` + +| Label key | Default | +|-----------|---------| +| `title` | Cookie preferences | +| `message` | We use cookies to improve your experience… | +| `policyLink` | Privacy policy | +| `accept` | Accept all | +| `reject` | Reject all | +| `manage` | Manage preferences | +| `save` | Save preferences | +| `necessaryTitle` | Necessary | +| `necessaryDescription` | Required for core site functionality… | +| `analyticsTitle` | Analytics | +| `analyticsDescription` | Helps us understand how visitors use the site… | +| `marketingTitle` | Marketing | +| `marketingDescription` | Used for ads and remarketing… | +| `alwaysOn` | Always on | +| `cookiesHeading` | View cookies | +| `cookieName` | Name | +| `cookiePurpose` | Purpose | +| `cookieDuration` | Duration | +| `cookieProvider` | Provider | +| `storageError` | Your preferences could not be saved… | + +### Theme colors + +Pass a `theme` object to override CSS variables without writing custom CSS: + +```jsx + +``` + +You can also pass `className` for additional styling hooks. + +### Cookie inventory table + +Provide a `cookies` array to show an expandable table inside each category in the preferences panel: + +```jsx + +``` + +If `cookies` is omitted, the banner keeps the simpler three-category view. + +### Error handling + +Consent write functions return a result object: + +```js +const result = acceptAll(); +if (!result.ok) { + console.error(result.reason); // "invalid" | "storage" +} +``` + +In React, use callbacks on the banner: + +```jsx + console.log("saved", consent)} + onConsentError={({ reason }) => console.warn("failed", reason)} +/> +``` + +Enable `debug: true` in `configureConsent()` to log warnings to the console, or use `onStorageError` for global handling. + +--- + +## Example app + +See [`examples/next-app`](examples/next-app) for a working Next.js demo with Turkish labels, custom theme colors, and a cookie inventory table. + +```bash +cd examples/next-app +npm install +npm run dev +``` + +--- | Banner category | User choice | Google Consent Mode signals | |-----------------|-------------|----------------------------| @@ -161,6 +300,7 @@ src/ ├── preferences.js Parse / serialize user choices ├── storage.js localStorage read/write ├── consent-api.js acceptAll, rejectAll, getConsent, … +├── cookie-schema.js Cookie inventory helpers ├── google/ │ ├── gtag.js dataLayer helpers │ ├── consent-signals.js Category → signal mapping @@ -169,6 +309,8 @@ src/ └── react/ ├── ConsentBootstrap.jsx ├── CookieConsent.jsx + ├── CookieList.jsx + ├── theme.js ├── GoogleTagManager.jsx └── FacebookPixel.jsx ``` @@ -189,9 +331,9 @@ src/ | Function | Description | |----------|-------------| | `getConsent()` | Read stored preferences | -| `setConsent({ analytics, marketing })` | Save custom choice | -| `acceptAll()` | Grant all categories | -| `rejectAll()` | Deny optional categories | +| `setConsent({ analytics, marketing })` | Save custom choice; returns `{ ok, consent \| reason }` | +| `acceptAll()` | Grant all categories; returns `{ ok, consent \| reason }` | +| `rejectAll()` | Deny optional categories; returns `{ ok, consent \| reason }` | | `hasAnalyticsConsent()` | Check analytics opt-in | | `hasMarketingConsent()` | Check marketing opt-in | | `openPreferences()` | Re-open banner panel | @@ -214,7 +356,18 @@ src/ | `EVENT_OPEN_PREFERENCES` | Preferences panel should open | | `GTM_CONSENT_EVENT` | Pushed to dataLayer (`cookie_consent_update`) | -Legacy aliases (`getCookieConsent`, `acceptAllCookies`, …) remain available. +### `CookieConsent` props + +| Prop | Type | Description | +|------|------|-------------| +| `labels` | `object` | Override banner text | +| `theme` | `object` | Override banner colors | +| `cookies` | `CookieEntry[]` | Optional cookie inventory table | +| `policyHref` | `string` | Privacy / cookie policy URL | +| `LinkComponent` | `component` | Custom link component (default: Next.js `Link`) | +| `className` | `string` | Extra CSS class on the banner root | +| `onConsentSaved` | `(consent) => void` | Called after a successful save | +| `onConsentError` | `({ reason }) => void` | Called when save fails | --- @@ -278,7 +431,17 @@ configureConsent({ --- -## Local development (link to another project) +## Troubleshooting + +| Problem | Likely cause | Fix | +|---------|--------------|-----| +| Banner never appears | Consent already stored | Clear `localStorage` key or call `openPreferences()` | +| GTM fires before consent | Bootstrap missing or loaded too late | Add `` with `beforeInteractive` before GTM | +| Consent not saved | Storage blocked (private mode, Safari ITP) | Handle `onConsentError`; show inline error from banner | +| Tags still fire after reject | GTM tags not using Consent Mode | Enable Consent Mode in GTM and set tag consent requirements | +| TypeScript errors on `./react` | Missing types path | Import from `@pdntechnology/cookie-consent/react` (types included since v1.1) | + +--- ```bash # In this repo @@ -312,11 +475,7 @@ npm publish --access public ## Contributing -Contributions are welcome. Please open an issue before large changes. - -1. Fork the repository -2. Create a feature branch -3. Submit a pull request +See [CONTRIBUTING.md](CONTRIBUTING.md). Bug reports and pull requests are welcome. --- diff --git a/README.tr.md b/README.tr.md new file mode 100644 index 0000000..aedf939 --- /dev/null +++ b/README.tr.md @@ -0,0 +1,344 @@ +# @pdntechnology/cookie-consent + +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) +[![npm version](https://img.shields.io/npm/v/@pdntechnology/cookie-consent.svg)](https://www.npmjs.com/package/@pdntechnology/cookie-consent) + +React ve Next.js için hafif, açık kaynak bir **Çerez Yönetim Platformu (CMP)**. [Google Consent Mode v2](https://developers.google.com/tag-platform/security/guides/consent) akışını sertifikalı araçlarla aynı şekilde uygular. + +Ağır bağımlılık yok. Vendor lock-in yok. Google Tag Manager, GA4, Google Ads ve Meta Pixel ile uyumludur. + +> English documentation: [README.md](README.md) + +--- + +## Neden var? + +Google sertifikalı CMP'ler (Cookiebot, OneTrust, CookieYes, …) aynı teknik akışı izler: + +1. Google etiketlerinden **önce** varsayılanları **reddedilmiş** olarak ayarla +2. GTM / gtag.js yükle +3. Çerez banner'ını göster +4. Kullanıcı seçimi → `gtag('consent', 'update', …)` + dataLayer olayı +5. Reklamlar reddedildiğinde → `ads_data_redaction` ve `url_passthrough` etkinleştir + +Bu kütüphane bu akışı okunabilir, yaklaşık 15 KB JavaScript ile uygular. + +> **Not:** Sertifikalı CMP'ler ayrıca IAB TCF v2.2 sinyalleri sağlar ve Google'ın sertifikalı satıcı listesinde yer alır. Bu kütüphane **Consent Mode v2 teknik entegrasyonunu** kapsar. Gizlilik politikası metni ve yasal uyumluluk sizin sorumluluğunuzdadır. + +--- + +## Özellikler + +- Google Consent Mode v2 (7 sinyalin tamamı) +- Granüler kategoriler: Zorunlu / Analitik / Pazarlama +- Özelleştirilebilir banner metni, tema renkleri ve çerez envanteri +- Bootstrap ve güncellemede `ads_data_redaction` + `url_passthrough` +- Bölgesel varsayılan onay (EEA vb.) +- React banner + Next.js bootstrap bileşeni +- İsteğe bağlı GTM ve Meta Pixel yükleyicileri (onay kapılı) +- Framework'ten bağımsız çekirdek API (vanilla JS desteklenir) +- Çekirdek API ve React bileşenleri için TypeScript tanımları +- Onay kaydedilemediğinde hata callback'leri + +--- + +## Kurulum + +```bash +npm install @pdntechnology/cookie-consent +``` + +### Next.js ayarı + +`next.config.mjs` dosyasına ekleyin: + +```js +const nextConfig = { + transpilePackages: ["@pdntechnology/cookie-consent"], +}; +``` + +--- + +## Hızlı başlangıç + +### 1. Yapılandırma (isteğe bağlı) + +```js +// lib/consent-config.js +import { configureConsent } from "@pdntechnology/cookie-consent"; + +configureConsent({ + storageKey: "site_cerez_tercihi", + waitForUpdateMs: 500, + adsDataRedaction: true, + urlPassthrough: true, + debug: false, +}); +``` + +Bu dosyayı root layout'ta `ConsentBootstrap`'ten **önce** import edin. + +### 2. Consent varsayılanlarını bootstrap et (GTM'den önce) + +```jsx +// app/layout.jsx +import "@pdntechnology/cookie-consent/styles.css"; +import "@/lib/consent-config"; +import { ConsentBootstrap } from "@pdntechnology/cookie-consent/react"; + +export default function RootLayout({ children }) { + return ( + + + + {children} + + + ); +} +``` + +### 3. Banner + etiketler + +```jsx +// app/providers.jsx +import Link from "next/link"; +import { + CookieConsent, + GoogleTagManager, +} from "@pdntechnology/cookie-consent/react"; + +export function Providers({ children }) { + return ( + <> + {children} + + + + ); +} +``` + +### 4. Tercihleri yeniden aç (footer linki) + +```jsx +import { openPreferences } from "@pdntechnology/cookie-consent"; + + +``` + +--- + +## Özelleştirme + +### Metin ve politika URL'si + +Görünen tüm metinler `labels` prop'u ile değiştirilebilir. Politika linki `policyHref` ile ayarlanır. + +| Label anahtarı | Varsayılan (EN) | +|----------------|-----------------| +| `title` | Cookie preferences | +| `message` | We use cookies to improve your experience… | +| `policyLink` | Privacy policy | +| `accept` | Accept all | +| `reject` | Reject all | +| `manage` | Manage preferences | +| `save` | Save preferences | +| `necessaryTitle` | Necessary | +| `necessaryDescription` | Required for core site functionality… | +| `analyticsTitle` | Analytics | +| `analyticsDescription` | Helps us understand how visitors use the site… | +| `marketingTitle` | Marketing | +| `marketingDescription` | Used for ads and remarketing… | +| `alwaysOn` | Always on | +| `cookiesHeading` | View cookies | +| `cookieName` | Name | +| `cookiePurpose` | Purpose | +| `cookieDuration` | Duration | +| `cookieProvider` | Provider | +| `storageError` | Your preferences could not be saved… | + +### Tema renkleri + +`theme` prop'u ile CSS değişkenlerini override edin: + +```jsx + +``` + +Desteklenen anahtarlar: `background`, `text`, `textMuted`, `link`, `primary`, `primaryText`, `secondaryBackground`, `border`, `accent`. + +### Çerez envanteri tablosu + +Hangi çerezlerin kullanıldığını tercihler panelinde göstermek için `cookies` dizisi verin: + +```jsx + +``` + +`cookies` verilmezse banner mevcut üç kategori görünümünü korur. + +### Hata yönetimi + +```js +const result = acceptAll(); +if (!result.ok) { + console.error(result.reason); // "invalid" | "storage" +} +``` + +React'te callback kullanın: + +```jsx + console.log("kaydedildi", consent)} + onConsentError={({ reason }) => console.warn("hata", reason)} +/> +``` + +`configureConsent({ debug: true })` ile konsola uyarı yazılır. Global işlem için `onStorageError` kullanılabilir. + +--- + +## Örnek uygulama + +[`examples/next-app`](examples/next-app) dizininde Türkçe metinler, özel tema ve çerez envanteri içeren çalışan bir Next.js demosu vardır. + +```bash +cd examples/next-app +npm install +npm run dev +``` + +--- + +## Kategoriler → Google sinyalleri + +| Banner kategorisi | Kullanıcı seçimi | Google Consent Mode sinyalleri | +|-------------------|------------------|--------------------------------| +| **Zorunlu** | Her zaman açık | `security_storage: granted` | +| **Analitik** | Opt-in | `analytics_storage` | +| **Pazarlama** | Opt-in | `ad_storage`, `ad_user_data`, `ad_personalization`, `functionality_storage`, `personalization_storage` | + +Pazarlama **reddedildiğinde** kütüphane ayrıca şunları ayarlar: + +- `ads_data_redaction: true` +- `url_passthrough: true` + +--- + +## API referansı + +### Yapılandırma + +| Fonksiyon | Açıklama | +|-----------|----------| +| `configureConsent(config)` | Depolama anahtarı, bekleme süresi, bölgesel varsayılanlar | +| `getConsentConfig()` | Aktif yapılandırmayı oku | + +### Kullanıcı onayı + +| Fonksiyon | Açıklama | +|-----------|----------| +| `getConsent()` | Kayıtlı tercihleri oku | +| `setConsent({ analytics, marketing })` | Özel seçim kaydet; `{ ok, consent \| reason }` döner | +| `acceptAll()` | Tüm kategorilere izin ver | +| `rejectAll()` | Opsiyonel kategorileri reddet | +| `hasAnalyticsConsent()` | Analitik onayını kontrol et | +| `hasMarketingConsent()` | Pazarlama onayını kontrol et | +| `openPreferences()` | Tercih panelini yeniden aç | +| `syncConsentFromStorage()` | GTM yüklendikten sonra kayıtlı onayı uygula | + +### `CookieConsent` prop'ları + +| Prop | Tip | Açıklama | +|------|-----|----------| +| `labels` | `object` | Banner metinlerini override et | +| `theme` | `object` | Banner renklerini override et | +| `cookies` | `CookieEntry[]` | Opsiyonel çerez envanteri tablosu | +| `policyHref` | `string` | Gizlilik / çerez politikası URL'si | +| `LinkComponent` | `component` | Özel link bileşeni (varsayılan: Next.js `Link`) | +| `className` | `string` | Banner kök elemanına ek CSS sınıfı | +| `onConsentSaved` | `(consent) => void` | Başarılı kayıttan sonra çağrılır | +| `onConsentError` | `({ reason }) => void` | Kayıt başarısız olursa çağrılır | + +--- + +## Sorun giderme + +| Sorun | Olası neden | Çözüm | +|-------|-------------|-------| +| Banner hiç görünmüyor | Onay zaten kayıtlı | `localStorage` anahtarını temizleyin veya `openPreferences()` çağırın | +| GTM onaydan önce tetikleniyor | Bootstrap eksik veya geç yükleniyor | GTM'den önce `` ekleyin | +| Onay kaydedilmiyor | Depolama engelli (gizli mod, Safari ITP) | `onConsentError` ile yakalayın; banner inline hata gösterir | +| Reddetmeden sonra etiketler çalışıyor | GTM etiketleri Consent Mode kullanmıyor | GTM'de Consent Mode'u etkinleştirin | +| `./react` TypeScript hatası | Eski sürüm | v1.1+ ile `@pdntechnology/cookie-consent/react` tipleri dahildir | + +--- + +## Katkıda bulunma + +Bkz. [CONTRIBUTING.md](CONTRIBUTING.md). + +--- + +## Lisans + +[MIT](LICENSE) © [PDN Technology](https://pdntechnology.com) diff --git a/examples/next-app/README.md b/examples/next-app/README.md new file mode 100644 index 0000000..5a823bc --- /dev/null +++ b/examples/next-app/README.md @@ -0,0 +1,15 @@ +# Next.js Demo + +Minimal demo for `@pdntechnology/cookie-consent`. + +## Run locally + +```bash +cd examples/next-app +npm install +npm run dev +``` + +Open [http://localhost:3000](http://localhost:3000). + +The demo uses a local file dependency on the parent package, Turkish labels, a custom theme, and a cookie inventory table. diff --git a/examples/next-app/app/layout.jsx b/examples/next-app/app/layout.jsx new file mode 100644 index 0000000..a448851 --- /dev/null +++ b/examples/next-app/app/layout.jsx @@ -0,0 +1,20 @@ +import "@pdntechnology/cookie-consent/styles.css"; +import "@/lib/consent-config"; +import { ConsentBootstrap } from "@pdntechnology/cookie-consent/react"; +import { Providers } from "./providers"; + +export const metadata = { + title: "Cookie Consent Demo", + description: "Next.js demo for @pdntechnology/cookie-consent", +}; + +export default function RootLayout({ children }) { + return ( + + + + {children} + + + ); +} diff --git a/examples/next-app/app/page.jsx b/examples/next-app/app/page.jsx new file mode 100644 index 0000000..fdcb7a1 --- /dev/null +++ b/examples/next-app/app/page.jsx @@ -0,0 +1,19 @@ +import { CookieSettingsButton } from "./providers"; + +export default function HomePage() { + return ( +
+

Cookie Consent Demo

+

+ Bu örnek uygulama, @pdntechnology/cookie-consent paketinin Next.js + ile nasıl kullanılacağını gösterir. Türkçe metinler, özel tema renkleri ve çerez + envanteri tablosu etkinleştirilmiştir. +

+

+ Sayfayı ilk kez açtığınızda banner görünür. Tercih kaydedildikten sonra aşağıdaki + buton ile paneli yeniden açabilirsiniz. +

+ +
+ ); +} diff --git a/examples/next-app/app/providers.jsx b/examples/next-app/app/providers.jsx new file mode 100644 index 0000000..a7a5f63 --- /dev/null +++ b/examples/next-app/app/providers.jsx @@ -0,0 +1,93 @@ +"use client"; + +import Link from "next/link"; +import { + CookieConsent, + openPreferences, +} from "@pdntechnology/cookie-consent/react"; + +const labels = { + title: "Çerez tercihleri", + message: "Deneyiminizi iyileştirmek ve site trafiğini analiz etmek için çerez kullanıyoruz.", + policyLink: "Çerez politikası", + accept: "Tümünü kabul et", + reject: "Tümünü reddet", + manage: "Tercihleri yönet", + save: "Tercihleri kaydet", + necessaryTitle: "Zorunlu", + necessaryDescription: "Sitenin temel işlevleri için gereklidir. Her zaman aktiftir.", + analyticsTitle: "Analitik", + analyticsDescription: "Google Analytics (_ga, _gid) ile ziyaretçi trafiğini ölçer.", + marketingTitle: "Pazarlama", + marketingDescription: "Google Ads ve Meta Pixel ile reklam performansını ölçer.", + alwaysOn: "Her zaman açık", + cookiesHeading: "Kullanılan çerezleri gör", + cookieName: "Ad", + cookiePurpose: "Amaç", + cookieDuration: "Süre", + cookieProvider: "Sağlayıcı", + storageError: "Tercihleriniz kaydedilemedi. Lütfen tekrar deneyin.", +}; + +const cookies = [ + { + category: "necessary", + name: "demo_cookie_consent", + purpose: "Çerez tercihlerinizi saklar", + duration: "1 yıl", + provider: "Bu site", + }, + { + category: "analytics", + name: "_ga", + purpose: "Ziyaretçi istatistikleri", + duration: "2 yıl", + provider: "Google Analytics", + }, + { + category: "analytics", + name: "_gid", + purpose: "Oturum ayırma", + duration: "24 saat", + provider: "Google Analytics", + }, + { + category: "marketing", + name: "_fbp", + purpose: "Reklam hedefleme", + duration: "3 ay", + provider: "Meta", + }, +]; + +export function Providers({ children }) { + return ( + <> + {children} + { + console.warn("[demo] consent error:", reason); + }} + /> + + ); +} + +export function CookieSettingsButton() { + return ( + + ); +} diff --git a/examples/next-app/jsconfig.json b/examples/next-app/jsconfig.json new file mode 100644 index 0000000..2a2e4b3 --- /dev/null +++ b/examples/next-app/jsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "paths": { + "@/*": ["./*"] + } + } +} diff --git a/examples/next-app/lib/consent-config.js b/examples/next-app/lib/consent-config.js new file mode 100644 index 0000000..28290ca --- /dev/null +++ b/examples/next-app/lib/consent-config.js @@ -0,0 +1,9 @@ +import { configureConsent } from "@pdntechnology/cookie-consent"; + +configureConsent({ + storageKey: "demo_cookie_consent", + waitForUpdateMs: 500, + adsDataRedaction: true, + urlPassthrough: true, + debug: false, +}); diff --git a/examples/next-app/next.config.mjs b/examples/next-app/next.config.mjs new file mode 100644 index 0000000..6e55a41 --- /dev/null +++ b/examples/next-app/next.config.mjs @@ -0,0 +1,7 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + transpilePackages: ["@pdntechnology/cookie-consent"], + outputFileTracingRoot: import.meta.dirname, +}; + +export default nextConfig; diff --git a/examples/next-app/package-lock.json b/examples/next-app/package-lock.json new file mode 100644 index 0000000..fbbaa2c --- /dev/null +++ b/examples/next-app/package-lock.json @@ -0,0 +1,928 @@ +{ + "name": "cookie-consent-next-example", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cookie-consent-next-example", + "version": "0.0.0", + "dependencies": { + "@pdntechnology/cookie-consent": "file:../..", + "next": "^15.3.3", + "react": "^19.1.0", + "react-dom": "^19.1.0" + } + }, + "../..": { + "name": "@pdntechnology/cookie-consent", + "version": "1.1.0", + "license": "MIT", + "devDependencies": { + "vitest": "^3.2.4" + }, + "peerDependencies": { + "next": ">=14.0.0", + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + }, + "peerDependenciesMeta": { + "next": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@next/env": { + "version": "15.5.19", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.19.tgz", + "integrity": "sha512-sWWluFvcv5v3Fxznmf2ZfjyoVQt/64oCnYqS90inQWGzMPK1VjvekPiz3OPHKmFT30EnHrjlbyaHLt3M0vWabw==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.5.19", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.19.tgz", + "integrity": "sha512-jx9wWlTKueHKPvVOndyr7WuaevWCkuYqsQ8gC0TMPKAVWG3MhcdMrjfo9tvIZNXd0QOUYXXvAcZ325y8Uq7uzg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.5.19", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.19.tgz", + "integrity": "sha512-291KFcsIQ3OenRdiUDFOR6W3wezzH4auENXm1gbm1Bjd4ANMMRgxPrWTUztQN43BnVoVuMnHCrLeECIMwgFKbA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.5.19", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.19.tgz", + "integrity": "sha512-WeH+nelQyyMeE2f8FxBRZNrGipya5zHZV2vjzfCOAYyiI6am+NbnWAAldOBFQBB2w0DjJcsvrKqoFT2b7+5YoA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.5.19", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.19.tgz", + "integrity": "sha512-5xTOE0lDlDCSSfp+BAif7j17VRRCjWp//ZPZy6NI0QpdrhxtQnsZguSx0xAAZ0c9XZLrLLwCe/XVe5YPrRilKw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.5.19", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.19.tgz", + "integrity": "sha512-LTxRmMgqqMv05Had879W00Fm53quiJd3Zuz8h1JSNJ3nGSlbZ/7Tjs1tKyScgN3Au3t3MyPsjPlq60fMmSHLsg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.5.19", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.19.tgz", + "integrity": "sha512-eoNQSpA5PQfB9wBO4RA47MTDXWz1fizy9Y3Z6e4DetYIF3dvjuu8sj7aIGn/bFCU6lnFzTK34NtCaffP4NsQ7Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.5.19", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.19.tgz", + "integrity": "sha512-6UNt2dFuCHOe446sm/Kp69nUe8/wIhnh9bm6Xcqw4qEWCOppLMOvhTBVgvM7invVUNr4SPpP6NOQsACtn2IN9Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.5.19", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.19.tgz", + "integrity": "sha512-PhmojAHyqMne56HBLGu9dhDnHPuFmEjrXSQMM/nW0J6j849lk3ESrVtqNJcCk8CKOV7brpTTbaYAjwKPzKM69w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@pdntechnology/cookie-consent": { + "resolved": "../..", + "link": true + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001799", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/nanoid": { + "version": "3.3.13", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.13.tgz", + "integrity": "sha512-sPdqC6ByMVVGvF1ynvvMo0/o+oD1VX7DaHhijt1bFgjvBkHBib4t49GoNDhf2NDta4oeUNlaGbSt5K7qjZ955Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "15.5.19", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.19.tgz", + "integrity": "sha512-xNOW6tYshGX1/Oi3F8uuk4gpDeWsSUE/1Z0G5uUMekIxaQ0xc03UXd9II0VQHYMWviMeA0OHpJFAKsHf8bTYVg==", + "license": "MIT", + "dependencies": { + "@next/env": "15.5.19", + "@swc/helpers": "0.5.15", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.5.19", + "@next/swc-darwin-x64": "15.5.19", + "@next/swc-linux-arm64-gnu": "15.5.19", + "@next/swc-linux-arm64-musl": "15.5.19", + "@next/swc-linux-x64-gnu": "15.5.19", + "@next/swc-linux-x64-musl": "15.5.19", + "@next/swc-win32-arm64-msvc": "15.5.19", + "@next/swc-win32-x64-msvc": "15.5.19", + "sharp": "^0.34.3" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + } + } +} diff --git a/examples/next-app/package.json b/examples/next-app/package.json new file mode 100644 index 0000000..9bb499b --- /dev/null +++ b/examples/next-app/package.json @@ -0,0 +1,16 @@ +{ + "name": "cookie-consent-next-example", + "private": true, + "version": "0.0.0", + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "@pdntechnology/cookie-consent": "file:../..", + "next": "^15.3.3", + "react": "^19.1.0", + "react-dom": "^19.1.0" + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..92d066e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1602 @@ +{ + "name": "@pdntechnology/cookie-consent", + "version": "1.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@pdntechnology/cookie-consent", + "version": "1.1.0", + "license": "MIT", + "devDependencies": { + "vitest": "^3.2.4" + }, + "peerDependencies": { + "next": ">=14.0.0", + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + }, + "peerDependenciesMeta": { + "next": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/expect": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.6.tgz", + "integrity": "sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.6", + "@vitest/utils": "3.2.6", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.6.tgz", + "integrity": "sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.6", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.6.tgz", + "integrity": "sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.6.tgz", + "integrity": "sha512-HYcoSj1w5tcgUnzoF0HcyaAQjpA1gj9ftUJ7iSJSuipc02jW9gKkigwZbjFldAfYHA1fa8UZVRftdMY5msWM9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.6", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.6.tgz", + "integrity": "sha512-H+ZjNTWGpObenh0YnlBctAPnJSI20P81PL8BPzWpx54YXLLTm8hEsWawtcYLMrwvpK48hGxLLbCS+1KRXhsKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.6", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.6.tgz", + "integrity": "sha512-oq6BbH68WzcWmwtBrU9nqLeaXTR4XwJF7FSLkKEZo4i6eoXcrxjcwSuTvWBIRUTC6VC72nXYunzqgZA+IKdtxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.6.tgz", + "integrity": "sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.6", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.13", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.13.tgz", + "integrity": "sha512-sPdqC6ByMVVGvF1ynvvMo0/o+oD1VX7DaHhijt1bFgjvBkHBib4t49GoNDhf2NDta4oeUNlaGbSt5K7qjZ955Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vite": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.5.tgz", + "integrity": "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.6.tgz", + "integrity": "sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.6", + "@vitest/mocker": "3.2.6", + "@vitest/pretty-format": "^3.2.6", + "@vitest/runner": "3.2.6", + "@vitest/snapshot": "3.2.6", + "@vitest/spy": "3.2.6", + "@vitest/utils": "3.2.6", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.6", + "@vitest/ui": "3.2.6", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/package.json b/package.json index aac0522..ad34901 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@pdntechnology/cookie-consent", - "version": "1.0.0", + "version": "1.1.0", "description": "Lightweight Google Consent Mode v2 CMP for React and Next.js", "type": "module", "main": "./src/index.js", @@ -9,18 +9,22 @@ "sideEffects": ["./src/styles.css"], "exports": { ".": { + "types": "./src/index.d.ts", "import": "./src/index.js", "default": "./src/index.js" }, "./react": { + "types": "./src/react/index.d.ts", "import": "./src/react/index.js", "default": "./src/react/index.js" }, "./styles.css": "./src/styles.css" }, - "files": ["src", "README.md", "LICENSE"], + "files": ["src", "README.md", "README.tr.md", "LICENSE", "CHANGELOG.md", "CONTRIBUTING.md"], "scripts": { - "prepublishOnly": "test -f README.md && test -f LICENSE" + "test": "vitest run", + "test:watch": "vitest", + "prepublishOnly": "npm test && test -f README.md && test -f LICENSE" }, "peerDependencies": { "next": ">=14.0.0", @@ -32,6 +36,9 @@ "react": { "optional": true }, "react-dom": { "optional": true } }, + "devDependencies": { + "vitest": "^3.2.4" + }, "keywords": [ "cookie-consent", "consent-mode", @@ -47,12 +54,12 @@ "license": "MIT", "repository": { "type": "git", - "url": "https://github.com/pdntechnology/cookie-consent.git" + "url": "https://git.pdntechnology.com.tr/pdn/cookie-consent.git" }, "bugs": { - "url": "https://github.com/pdntechnology/cookie-consent/issues" + "url": "https://git.pdntechnology.com.tr/pdn/cookie-consent/issues" }, - "homepage": "https://github.com/pdntechnology/cookie-consent#readme", + "homepage": "https://git.pdntechnology.com.tr/pdn/cookie-consent#readme", "publishConfig": { "access": "public" } diff --git a/src/consent-api.js b/src/consent-api.js index 96ed011..a4da7c2 100644 --- a/src/consent-api.js +++ b/src/consent-api.js @@ -1,3 +1,4 @@ +import { getConsentConfig } from "./config.js"; import { EVENT_CONSENT_UPDATED, EVENT_OPEN_PREFERENCES } from "./constants.js"; import { applyConsentUpdate } from "./google/consent-lifecycle.js"; import { parseUserConsent } from "./preferences.js"; @@ -23,26 +24,63 @@ export function hasMarketingConsent() { // ─── Write ────────────────────────────────────────────────────────────────── +/** + * @param {unknown} reason + * @param {unknown} [detail] + */ +function reportConsentError(reason, detail) { + const config = getConsentConfig(); + + if (config.debug) { + console.warn("[cookie-consent]", reason, detail ?? ""); + } + + if (typeof config.onStorageError === "function") { + config.onStorageError({ reason, detail }); + } +} + +/** + * @param {import("./types.js").UserConsent | Partial | "accepted" | "rejected"} preferences + * @returns {import("./types.js").PersistResult} + */ function persistConsent(preferences) { const consent = parseUserConsent(preferences); - if (!consent) return; + if (!consent) { + reportConsentError("invalid", preferences); + return { ok: false, reason: "invalid" }; + } - if (!writeStoredConsent(consent)) return; + if (!writeStoredConsent(consent)) { + reportConsentError("storage", consent); + return { ok: false, reason: "storage" }; + } applyConsentUpdate(consent); window.dispatchEvent(new CustomEvent(EVENT_CONSENT_UPDATED, { detail: consent })); + return { ok: true, consent }; } +/** + * @param {import("./types.js").UserConsent | Partial} preferences + * @returns {import("./types.js").PersistResult} + */ export function setConsent(preferences) { - persistConsent(preferences); + return persistConsent(preferences); } +/** + * @returns {import("./types.js").PersistResult} + */ export function acceptAll() { - persistConsent({ analytics: true, marketing: true }); + return persistConsent({ analytics: true, marketing: true }); } +/** + * @returns {import("./types.js").PersistResult} + */ export function rejectAll() { - persistConsent({ analytics: false, marketing: false }); + return persistConsent({ analytics: false, marketing: false }); } // ─── Sync & UI ────────────────────────────────────────────────────────────── diff --git a/src/consent-api.test.js b/src/consent-api.test.js new file mode 100644 index 0000000..2dc04dc --- /dev/null +++ b/src/consent-api.test.js @@ -0,0 +1,61 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { configureConsent } from "./config.js"; +import { EVENT_CONSENT_UPDATED } from "./constants.js"; +import { acceptAll, rejectAll, setConsent } from "./consent-api.js"; + +const storage = new Map(); +const dataLayer = []; + +describe("consent-api", () => { + beforeEach(() => { + storage.clear(); + dataLayer.length = 0; + configureConsent({ storageKey: "test_consent", debug: false }); + + vi.stubGlobal("window", { + localStorage: { + getItem: (key) => storage.get(key) ?? null, + setItem: (key, value) => { + storage.set(key, value); + }, + }, + dataLayer, + dispatchEvent: vi.fn((event) => { + window.__lastEvent = event; + }), + }); + + vi.stubGlobal("dataLayer", dataLayer); + window.dataLayer = dataLayer; + window.gtag = (...args) => { + dataLayer.push(args); + }; + }); + + it("acceptAll persists consent and dispatches event", () => { + const result = acceptAll(); + + expect(result).toEqual({ ok: true, consent: { analytics: true, marketing: true } }); + expect(window.__lastEvent.type).toBe(EVENT_CONSENT_UPDATED); + expect(dataLayer.some((entry) => entry[0] === "consent" && entry[1] === "update")).toBe(true); + }); + + it("rejectAll denies optional categories", () => { + const result = rejectAll(); + expect(result).toEqual({ ok: true, consent: { analytics: false, marketing: false } }); + }); + + it("returns invalid when preferences cannot be parsed", () => { + const result = setConsent(null); + expect(result).toEqual({ ok: false, reason: "invalid" }); + }); + + it("returns storage error when localStorage write fails", () => { + window.localStorage.setItem = () => { + throw new Error("blocked"); + }; + + const result = setConsent({ analytics: true, marketing: true }); + expect(result).toEqual({ ok: false, reason: "storage" }); + }); +}); diff --git a/src/cookie-schema.js b/src/cookie-schema.js new file mode 100644 index 0000000..af6bdaa --- /dev/null +++ b/src/cookie-schema.js @@ -0,0 +1,57 @@ +/** @typedef {"necessary"|"analytics"|"marketing"} CookieCategory */ + +/** + * @typedef {Object} CookieEntry + * @property {CookieCategory} category + * @property {string} name + * @property {string} purpose + * @property {string} duration + * @property {string} provider + */ + +export const COOKIE_CATEGORIES = ["necessary", "analytics", "marketing"]; + +/** + * @param {unknown} entry + * @returns {entry is CookieEntry} + */ +export function isValidCookieEntry(entry) { + if (!entry || typeof entry !== "object") return false; + const { category, name, purpose, duration, provider } = entry; + return ( + COOKIE_CATEGORIES.includes(category) && + typeof name === "string" && + name.length > 0 && + typeof purpose === "string" && + typeof duration === "string" && + typeof provider === "string" + ); +} + +/** + * @param {unknown[]} cookies + * @returns {CookieEntry[]} + */ +export function normalizeCookieEntries(cookies) { + if (!Array.isArray(cookies)) return []; + return cookies.filter(isValidCookieEntry); +} + +/** + * @param {CookieEntry[]} cookies + * @returns {Record} + */ +export function groupCookiesByCategory(cookies) { + /** @type {Record} */ + const grouped = { + necessary: [], + analytics: [], + marketing: [], + }; + + for (const entry of cookies) { + grouped[entry.category].push(entry); + } + + return grouped; +} diff --git a/src/cookie-schema.test.js b/src/cookie-schema.test.js new file mode 100644 index 0000000..713212c --- /dev/null +++ b/src/cookie-schema.test.js @@ -0,0 +1,61 @@ +import { describe, expect, it } from "vitest"; +import { + groupCookiesByCategory, + isValidCookieEntry, + normalizeCookieEntries, +} from "./cookie-schema.js"; + +describe("cookie-schema", () => { + it("validates cookie entries", () => { + expect( + isValidCookieEntry({ + category: "analytics", + name: "_ga", + purpose: "Analytics", + duration: "2 years", + provider: "Google", + }), + ).toBe(true); + + expect(isValidCookieEntry({ category: "unknown", name: "_ga" })).toBe(false); + }); + + it("normalizes invalid entries out of the list", () => { + const cookies = normalizeCookieEntries([ + { + category: "marketing", + name: "_fbp", + purpose: "Ads", + duration: "3 months", + provider: "Meta", + }, + { category: "invalid", name: "bad" }, + ]); + + expect(cookies).toHaveLength(1); + expect(cookies[0].name).toBe("_fbp"); + }); + + it("groups cookies by category", () => { + const grouped = groupCookiesByCategory([ + { + category: "analytics", + name: "_ga", + purpose: "Analytics", + duration: "2 years", + provider: "Google", + }, + { + category: "necessary", + name: "ccm_consent", + purpose: "Preferences", + duration: "1 year", + provider: "Site", + }, + ]); + + expect(grouped.analytics).toHaveLength(1); + expect(grouped.necessary).toHaveLength(1); + expect(grouped.marketing).toHaveLength(0); + }); +}); diff --git a/src/google/bootstrap-script.js b/src/google/bootstrap-script.js index af0ff00..f967eae 100644 --- a/src/google/bootstrap-script.js +++ b/src/google/bootstrap-script.js @@ -1,24 +1,38 @@ import { getConsentConfig } from "../config.js"; import { getDefaultDeniedState } from "./consent-signals.js"; +function buildBootstrapDefaults(config) { + const wait = Number(config.waitForUpdateMs) || 500; + const defaults = getDefaultDeniedState(wait); + + if (config.adsDataRedaction !== false) { + defaults.ads_data_redaction = true; + } + if (config.urlPassthrough !== false) { + defaults.url_passthrough = true; + } + + return defaults; +} + /** * Inline script for `` — must execute before GTM snippet. * Used by ConsentBootstrap and vanilla HTML integrations. */ export function getConsentBootstrapScript(overrides = {}) { const config = { ...getConsentConfig(), ...overrides }; - const wait = Number(config.waitForUpdateMs) || 500; + const bootstrapDefaults = buildBootstrapDefaults(config); const regionLines = (config.regionDefaults || []) .filter((block) => Array.isArray(block.regions) && block.regions.length > 0) .map((block) => { const { regions, ...signals } = block; - const payload = JSON.stringify({ ...getDefaultDeniedState(wait), ...signals, region: regions }); + const payload = JSON.stringify({ ...bootstrapDefaults, ...signals, region: regions }); return `gtag('consent','default',${payload});`; }) .join("\n"); - const globalDefault = JSON.stringify(getDefaultDeniedState(wait)); + const globalDefault = JSON.stringify(bootstrapDefaults); return ` window.dataLayer=window.dataLayer||[]; diff --git a/src/google/bootstrap-script.test.js b/src/google/bootstrap-script.test.js new file mode 100644 index 0000000..ae407ea --- /dev/null +++ b/src/google/bootstrap-script.test.js @@ -0,0 +1,23 @@ +import { describe, expect, it } from "vitest"; +import { configureConsent } from "../config.js"; +import { getConsentBootstrapScript } from "./bootstrap-script.js"; + +describe("getConsentBootstrapScript", () => { + it("includes denied defaults and advanced settings", () => { + configureConsent({ + waitForUpdateMs: 500, + adsDataRedaction: true, + urlPassthrough: true, + regionDefaults: [], + }); + + const script = getConsentBootstrapScript(); + + expect(script).toContain("gtag('consent','default'"); + expect(script).toContain('"analytics_storage":"denied"'); + expect(script).toContain('"security_storage":"granted"'); + expect(script).toContain('"ads_data_redaction":true'); + expect(script).toContain('"url_passthrough":true'); + expect(script).toContain('"wait_for_update":500'); + }); +}); diff --git a/src/google/consent-signals.test.js b/src/google/consent-signals.test.js new file mode 100644 index 0000000..bbe1d15 --- /dev/null +++ b/src/google/consent-signals.test.js @@ -0,0 +1,64 @@ +import { describe, expect, it } from "vitest"; +import { buildConsentSignals, buildAdvancedSettings, getDefaultDeniedState } from "./consent-signals.js"; + +describe("buildConsentSignals", () => { + it("denies optional categories by default", () => { + expect(buildConsentSignals(null)).toEqual({ + analytics_storage: "denied", + ad_storage: "denied", + ad_user_data: "denied", + ad_personalization: "denied", + functionality_storage: "denied", + personalization_storage: "denied", + security_storage: "granted", + }); + }); + + it("grants analytics signals when analytics is true", () => { + const signals = buildConsentSignals({ analytics: true, marketing: false }); + expect(signals.analytics_storage).toBe("granted"); + expect(signals.ad_storage).toBe("denied"); + }); + + it("grants marketing signals when marketing is true", () => { + const signals = buildConsentSignals({ analytics: false, marketing: true }); + expect(signals.analytics_storage).toBe("denied"); + expect(signals.ad_storage).toBe("granted"); + expect(signals.ad_user_data).toBe("granted"); + expect(signals.ad_personalization).toBe("granted"); + expect(signals.functionality_storage).toBe("granted"); + expect(signals.personalization_storage).toBe("granted"); + }); +}); + +describe("buildAdvancedSettings", () => { + it("enables redaction when marketing is denied", () => { + expect( + buildAdvancedSettings({ marketing: false }, { + adsDataRedaction: true, + urlPassthrough: true, + }), + ).toEqual({ + ads_data_redaction: true, + url_passthrough: true, + }); + }); + + it("disables redaction when marketing is granted", () => { + expect( + buildAdvancedSettings({ marketing: true }, { + adsDataRedaction: true, + urlPassthrough: true, + }), + ).toEqual({ + ads_data_redaction: false, + url_passthrough: false, + }); + }); +}); + +describe("getDefaultDeniedState", () => { + it("includes wait_for_update", () => { + expect(getDefaultDeniedState(750).wait_for_update).toBe(750); + }); +}); diff --git a/src/index.d.ts b/src/index.d.ts index 90ebc29..81e58d5 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -21,8 +21,21 @@ export interface ConsentConfig { urlPassthrough?: boolean; regionDefaults?: RegionDefault[]; debug?: boolean; + onStorageError?: (error: { reason: "invalid" | "storage"; detail?: unknown }) => void; } +export interface ConsentResult { + ok: true; + consent: UserConsent; +} + +export interface ConsentError { + ok: false; + reason: "invalid" | "storage"; +} + +export type PersistResult = ConsentResult | ConsentError; + export const DEFAULT_STORAGE_KEY: string; export const EVENT_CONSENT_UPDATED: string; export const EVENT_OPEN_PREFERENCES: string; @@ -34,9 +47,9 @@ export function getConsentConfig(): ConsentConfig; export function defaultConfig: ConsentConfig; export function getConsent(): UserConsent | null; -export function setConsent(preferences: Partial): void; -export function acceptAll(): void; -export function rejectAll(): void; +export function setConsent(preferences: Partial): PersistResult; +export function acceptAll(): PersistResult; +export function rejectAll(): PersistResult; export function hasAnalyticsConsent(): boolean; export function hasMarketingConsent(): boolean; export function hasConsentChoice(): boolean; @@ -48,15 +61,31 @@ export function applyConsentUpdate(consent: UserConsent | null | undefined): voi export function buildGoogleConsentState(consent: UserConsent | null | undefined): Record; export function getConsentBootstrapScript(options?: Partial): string; +export type CookieCategory = "necessary" | "analytics" | "marketing"; + +export interface CookieEntry { + category: CookieCategory; + name: string; + purpose: string; + duration: string; + provider: string; +} + +export const COOKIE_CATEGORIES: readonly CookieCategory[]; + +export function isValidCookieEntry(entry: unknown): entry is CookieEntry; +export function normalizeCookieEntries(cookies: unknown[]): CookieEntry[]; +export function groupCookiesByCategory(cookies: CookieEntry[]): Record; + // Legacy aliases export const COOKIE_CONSENT_KEY: string; export const COOKIE_CONSENT_EVENT: string; export const COOKIE_CONSENT_OPEN_EVENT: string; export const CONSENT_TYPES: readonly string[]; export function getCookieConsent(): UserConsent | null; -export function setCookieConsent(preferences: Partial): void; -export function acceptAllCookies(): void; -export function rejectAllCookies(): void; +export function setCookieConsent(preferences: Partial): PersistResult; +export function acceptAllCookies(): PersistResult; +export function rejectAllCookies(): PersistResult; export function hasCookieConsentChoice(): boolean; export function syncGoogleConsentFromStorage(): void; export function openCookiePreferences(): void; diff --git a/src/index.js b/src/index.js index 6cb4ccd..6be0758 100644 --- a/src/index.js +++ b/src/index.js @@ -36,6 +36,14 @@ export { openCookiePreferences, } from "./consent-api.js"; +// Cookie inventory helpers +export { + COOKIE_CATEGORIES, + groupCookiesByCategory, + isValidCookieEntry, + normalizeCookieEntries, +} from "./cookie-schema.js"; + // Google Consent Mode export { applyConsentDefaults, applyConsentUpdate } from "./google/consent-lifecycle.js"; export { buildConsentSignals as buildGoogleConsentState } from "./google/consent-signals.js"; diff --git a/src/preferences.test.js b/src/preferences.test.js new file mode 100644 index 0000000..f480308 --- /dev/null +++ b/src/preferences.test.js @@ -0,0 +1,34 @@ +import { describe, expect, it } from "vitest"; +import { parseUserConsent, serializeUserConsent } from "./preferences.js"; + +describe("parseUserConsent", () => { + it("parses legacy accepted/rejected strings", () => { + expect(parseUserConsent("accepted")).toEqual({ analytics: true, marketing: true }); + expect(parseUserConsent("rejected")).toEqual({ analytics: false, marketing: false }); + }); + + it("normalises partial objects", () => { + expect(parseUserConsent({ analytics: 1, marketing: 0 })).toEqual({ + analytics: true, + marketing: false, + }); + }); + + it("returns null for invalid values", () => { + expect(parseUserConsent(null)).toBeNull(); + expect(parseUserConsent(undefined)).toBeNull(); + expect(parseUserConsent("maybe")).toBeNull(); + }); +}); + +describe("serializeUserConsent", () => { + it("stores version and timestamp metadata", () => { + const raw = serializeUserConsent({ analytics: true, marketing: false }); + const parsed = JSON.parse(raw); + + expect(parsed.analytics).toBe(true); + expect(parsed.marketing).toBe(false); + expect(parsed.version).toBe(1); + expect(typeof parsed.updatedAt).toBe("string"); + }); +}); diff --git a/src/react/CookieConsent.jsx b/src/react/CookieConsent.jsx index a7659dd..b7e9df1 100644 --- a/src/react/CookieConsent.jsx +++ b/src/react/CookieConsent.jsx @@ -1,7 +1,9 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import Link from "next/link"; +import { groupCookiesByCategory, normalizeCookieEntries } from "../cookie-schema.js"; +import { getStorageKey } from "../config.js"; import { EVENT_OPEN_PREFERENCES } from "../constants.js"; import { acceptAll, @@ -10,6 +12,8 @@ import { rejectAll, setConsent, } from "../consent-api.js"; +import CookieList from "./CookieList.jsx"; +import { buildThemeStyle } from "./theme.js"; const DEFAULT_LABELS = { title: "Cookie preferences", @@ -26,6 +30,12 @@ const DEFAULT_LABELS = { marketingTitle: "Marketing", marketingDescription: "Used for ads and remarketing (Google Ads, Meta Pixel).", alwaysOn: "Always on", + cookiesHeading: "View cookies", + cookieName: "Name", + cookiePurpose: "Purpose", + cookieDuration: "Duration", + cookieProvider: "Provider", + storageError: "Your preferences could not be saved. Please try again.", }; /** @@ -34,16 +44,29 @@ const DEFAULT_LABELS = { */ export default function CookieConsent({ labels: labelOverrides, + theme, + cookies, policyHref = "/privacy", LinkComponent = Link, + className = "", + onConsentSaved, + onConsentError, }) { const labels = { ...DEFAULT_LABELS, ...labelOverrides }; const PolicyLink = LinkComponent; + const themeStyle = useMemo(() => buildThemeStyle(theme), [theme]); + const groupedCookies = useMemo( + () => groupCookiesByCategory(normalizeCookieEntries(cookies)), + [cookies], + ); + const hasCookieInventory = Boolean(cookies?.length); + const bannerRef = useRef(null); const [visible, setVisible] = useState(false); const [mode, setMode] = useState("banner"); const [analytics, setAnalytics] = useState(false); const [marketing, setMarketing] = useState(false); + const [errorMessage, setErrorMessage] = useState(""); useEffect(() => { setVisible(!hasConsentChoice()); @@ -56,42 +79,110 @@ export default function CookieConsent({ setMarketing(current?.marketing ?? false); setMode("preferences"); setVisible(true); + setErrorMessage(""); } window.addEventListener(EVENT_OPEN_PREFERENCES, handleOpenPreferences); return () => window.removeEventListener(EVENT_OPEN_PREFERENCES, handleOpenPreferences); }, []); + useEffect(() => { + function handleStorage(event) { + if (event.key !== getStorageKey()) return; + + if (event.newValue) { + setVisible(false); + setErrorMessage(""); + return; + } + + setMode("banner"); + setVisible(true); + } + + window.addEventListener("storage", handleStorage); + return () => window.removeEventListener("storage", handleStorage); + }, []); + + useEffect(() => { + if (!visible) return; + + const firstFocusable = bannerRef.current?.querySelector( + 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])', + ); + firstFocusable?.focus(); + }, [visible, mode]); + + useEffect(() => { + if (!visible) return; + + function handleKeyDown(event) { + if (event.key !== "Escape") return; + + if (mode === "preferences" && !hasConsentChoice()) { + setMode("banner"); + setErrorMessage(""); + return; + } + + setVisible(false); + setErrorMessage(""); + } + + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [visible, mode]); + if (!visible) return null; function close() { setVisible(false); + setErrorMessage(""); + } + + function handlePersistResult(result) { + if (result.ok) { + setErrorMessage(""); + onConsentSaved?.(result.consent); + close(); + return; + } + + setErrorMessage(labels.storageError); + onConsentError?.({ reason: result.reason }); } function handleAcceptAll() { - acceptAll(); - close(); + handlePersistResult(acceptAll()); } function handleRejectAll() { - rejectAll(); - close(); + handlePersistResult(rejectAll()); } function handleSave() { - setConsent({ analytics, marketing }); - close(); + handlePersistResult(setConsent({ analytics, marketing })); } + const bannerClassName = ["ccm-banner", className].filter(Boolean).join(" "); + return (
+ {errorMessage ? ( +

+ {errorMessage} +

+ ) : null} + {mode === "banner" ? ( setMode("preferences")} + onManage={() => { + setMode("preferences"); + setErrorMessage(""); + }} /> ) : (
@@ -200,7 +304,7 @@ function PreferencesView({ ); } -function CategoryCard({ title, description, checked, onChange, locked, badge }) { +function CategoryCard({ title, description, checked, onChange, locked, badge, cookies, labels }) { if (locked) { return (
@@ -209,6 +313,7 @@ function CategoryCard({ title, description, checked, onChange, locked, badge }) {badge}

{description}

+ {cookies ? : null}
); } @@ -220,6 +325,7 @@ function CategoryCard({ title, description, checked, onChange, locked, badge }) onChange(e.target.checked)} />

{description}

+ {cookies ? : null} ); } diff --git a/src/react/CookieList.jsx b/src/react/CookieList.jsx new file mode 100644 index 0000000..961a35b --- /dev/null +++ b/src/react/CookieList.jsx @@ -0,0 +1,32 @@ +/** + * Expandable cookie inventory table for a single category. + */ +export default function CookieList({ cookies, labels }) { + if (!cookies?.length) return null; + + return ( +
+ {labels.cookiesHeading} + + + + + + + + + + + {cookies.map((cookie) => ( + + + + + + + ))} + +
{labels.cookieName}{labels.cookiePurpose}{labels.cookieDuration}{labels.cookieProvider}
{cookie.name}{cookie.purpose}{cookie.duration}{cookie.provider}
+
+ ); +} diff --git a/src/react/index.d.ts b/src/react/index.d.ts new file mode 100644 index 0000000..4c85752 --- /dev/null +++ b/src/react/index.d.ts @@ -0,0 +1,89 @@ +import type { ComponentType, CSSProperties } from "react"; +import type { + ConsentConfig, + CookieEntry, + PersistResult, + UserConsent, +} from "../index.js"; + +export interface CookieConsentLabels { + title?: string; + message?: string; + policyLink?: string; + accept?: string; + reject?: string; + manage?: string; + save?: string; + necessaryTitle?: string; + necessaryDescription?: string; + analyticsTitle?: string; + analyticsDescription?: string; + marketingTitle?: string; + marketingDescription?: string; + alwaysOn?: string; + cookiesHeading?: string; + cookieName?: string; + cookiePurpose?: string; + cookieDuration?: string; + cookieProvider?: string; + storageError?: string; +} + +export interface CookieConsentTheme { + background?: string; + text?: string; + textMuted?: string; + link?: string; + primary?: string; + primaryText?: string; + secondaryBackground?: string; + border?: string; + accent?: string; +} + +export interface CookieConsentProps { + labels?: Partial; + theme?: CookieConsentTheme; + cookies?: CookieEntry[]; + policyHref?: string; + LinkComponent?: ComponentType<{ href: string; children: React.ReactNode }>; + className?: string; + onConsentSaved?: (consent: UserConsent) => void; + onConsentError?: (error: { reason: "invalid" | "storage" }) => void; +} + +export interface ConsentBootstrapProps { + overrides?: Partial; +} + +export interface GoogleTagManagerProps { + gtmId: string; +} + +export interface FacebookPixelProps { + pixelId: string; +} + +export const CookieConsent: ComponentType; +export const ConsentBootstrap: ComponentType; +export const GoogleTagManager: ComponentType; +export const FacebookPixel: ComponentType; + +export function configureConsent(config?: ConsentConfig): ConsentConfig; +export function getConsent(): UserConsent | null; +export function setConsent(preferences: Partial): PersistResult; +export function acceptAll(): PersistResult; +export function rejectAll(): PersistResult; +export function hasAnalyticsConsent(): boolean; +export function hasMarketingConsent(): boolean; +export function openPreferences(): void; +export function syncConsentFromStorage(): void; + +export function getCookieConsent(): UserConsent | null; +export function setCookieConsent(preferences: Partial): PersistResult; +export function acceptAllCookies(): PersistResult; +export function rejectAllCookies(): PersistResult; +export function openCookiePreferences(): void; +export function syncGoogleConsentFromStorage(): void; + +export function buildThemeStyle(theme?: CookieConsentTheme): CSSProperties; diff --git a/src/react/index.js b/src/react/index.js index f9400b2..c476584 100644 --- a/src/react/index.js +++ b/src/react/index.js @@ -2,6 +2,7 @@ export { default as CookieConsent } from "./CookieConsent.jsx"; export { default as ConsentBootstrap } from "./ConsentBootstrap.jsx"; export { default as GoogleTagManager } from "./GoogleTagManager.jsx"; export { default as FacebookPixel } from "./FacebookPixel.jsx"; +export { buildThemeStyle, THEME_VAR_MAP } from "./theme.js"; export { configureConsent } from "../config.js"; export { diff --git a/src/react/theme.js b/src/react/theme.js new file mode 100644 index 0000000..597167a --- /dev/null +++ b/src/react/theme.js @@ -0,0 +1,29 @@ +/** Maps theme prop keys to CSS custom property names. */ +export const THEME_VAR_MAP = { + background: "--ccm-bg", + text: "--ccm-text", + textMuted: "--ccm-text-muted", + link: "--ccm-link", + primary: "--ccm-primary", + primaryText: "--ccm-primary-text", + secondaryBackground: "--ccm-secondary-bg", + border: "--ccm-border", + accent: "--ccm-accent", +}; + +/** + * Build inline style object for CSS variable overrides. + * @param {Record|undefined} theme + * @returns {Record} + */ +export function buildThemeStyle(theme) { + if (!theme || typeof theme !== "object") return {}; + + const style = {}; + for (const [key, value] of Object.entries(theme)) { + if (value == null || value === "") continue; + const cssVar = THEME_VAR_MAP[key]; + if (cssVar) style[cssVar] = value; + } + return style; +} diff --git a/src/react/theme.test.js b/src/react/theme.test.js new file mode 100644 index 0000000..79b59c8 --- /dev/null +++ b/src/react/theme.test.js @@ -0,0 +1,24 @@ +import { describe, expect, it } from "vitest"; +import { buildThemeStyle, THEME_VAR_MAP } from "./theme.js"; + +describe("buildThemeStyle", () => { + it("maps theme keys to CSS custom properties", () => { + expect( + buildThemeStyle({ + primary: "#2563eb", + background: "#0f172a", + }), + ).toEqual({ + [THEME_VAR_MAP.primary]: "#2563eb", + [THEME_VAR_MAP.background]: "#0f172a", + }); + }); + + it("ignores empty values", () => { + expect(buildThemeStyle({ primary: "", text: null })).toEqual({}); + }); + + it("returns empty object when theme is missing", () => { + expect(buildThemeStyle()).toEqual({}); + }); +}); diff --git a/src/storage.test.js b/src/storage.test.js new file mode 100644 index 0000000..bca8b76 --- /dev/null +++ b/src/storage.test.js @@ -0,0 +1,41 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { configureConsent } from "./config.js"; +import { readStoredConsent, writeStoredConsent } from "./storage.js"; + +const storage = new Map(); + +describe("storage", () => { + beforeEach(() => { + storage.clear(); + configureConsent({ storageKey: "test_consent" }); + + vi.stubGlobal("window", { + localStorage: { + getItem: (key) => storage.get(key) ?? null, + setItem: (key, value) => { + storage.set(key, value); + }, + }, + }); + }); + + it("returns null when nothing is stored", () => { + expect(readStoredConsent()).toBeNull(); + }); + + it("reads and writes consent objects", () => { + writeStoredConsent({ analytics: true, marketing: false }); + expect(readStoredConsent()).toEqual({ analytics: true, marketing: false }); + }); + + it("falls back to legacy plain string values", () => { + storage.set("test_consent", "accepted"); + expect(readStoredConsent()).toEqual({ analytics: true, marketing: true }); + }); + + it("returns null on SSR", () => { + vi.stubGlobal("window", undefined); + expect(readStoredConsent()).toBeNull(); + expect(writeStoredConsent({ analytics: true, marketing: true })).toBe(false); + }); +}); diff --git a/src/styles.css b/src/styles.css index 2328fa3..bdc3307 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1,14 +1,24 @@ /* Cookie Consent Module — banner styles */ .ccm-banner { + --ccm-bg: rgba(5, 22, 52, 0.96); + --ccm-text: #ffffff; + --ccm-text-muted: rgba(255, 255, 255, 0.82); + --ccm-link: #ffffff; + --ccm-primary: #4a90d9; + --ccm-primary-text: #ffffff; + --ccm-secondary-bg: rgba(255, 255, 255, 0.1); + --ccm-border: rgba(255, 255, 255, 0.14); + --ccm-accent: #4a90d9; + position: fixed; left: 0; right: 0; bottom: 0; z-index: 10050; padding: 16px; - background: rgba(5, 22, 52, 0.96); - color: #fff; + background: var(--ccm-bg); + color: var(--ccm-text); box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.2); } @@ -30,7 +40,7 @@ margin: 0 0 8px; font-size: 18px; font-weight: 700; - color: #fff; + color: var(--ccm-text); } .ccm-banner__text { @@ -42,10 +52,22 @@ } .ccm-banner__text a { - color: #fff; + color: var(--ccm-link); text-decoration: underline; } +.ccm-banner__error { + flex: 1 1 100%; + margin: 0; + padding: 10px 14px; + border-radius: 8px; + font-size: 13px; + line-height: 1.45; + color: #fecaca; + background: rgba(220, 38, 38, 0.18); + border: 1px solid rgba(248, 113, 113, 0.35); +} + .ccm-banner__categories { flex: 1 1 100%; display: grid; @@ -56,7 +78,7 @@ margin: 0; padding: 12px 14px; border-radius: 10px; - border: 1px solid rgba(255, 255, 255, 0.14); + border: 1px solid var(--ccm-border); background: rgba(255, 255, 255, 0.04); cursor: pointer; } @@ -70,7 +92,7 @@ margin: 6px 0 0; font-size: 13px; line-height: 1.45; - color: rgba(255, 255, 255, 0.82); + color: var(--ccm-text-muted); } .ccm-banner__category-head { @@ -88,7 +110,7 @@ .ccm-banner__category-head input[type="checkbox"] { width: 18px; height: 18px; - accent-color: #4a90d9; + accent-color: var(--ccm-accent); cursor: pointer; } @@ -103,6 +125,54 @@ color: rgba(255, 255, 255, 0.9); } +.ccm-banner__cookies { + margin-top: 10px; +} + +.ccm-banner__cookies summary { + cursor: pointer; + font-size: 12px; + font-weight: 600; + color: var(--ccm-link); + list-style: none; +} + +.ccm-banner__cookies summary::-webkit-details-marker { + display: none; +} + +.ccm-banner__cookies summary::before { + content: "▸ "; +} + +.ccm-banner__cookies[open] summary::before { + content: "▾ "; +} + +.ccm-banner__cookie-table { + width: 100%; + margin-top: 8px; + border-collapse: collapse; + font-size: 12px; +} + +.ccm-banner__cookie-table th, +.ccm-banner__cookie-table td { + padding: 6px 8px; + text-align: left; + border-bottom: 1px solid var(--ccm-border); + vertical-align: top; +} + +.ccm-banner__cookie-table th { + font-weight: 600; + color: var(--ccm-text); +} + +.ccm-banner__cookie-table td { + color: var(--ccm-text-muted); +} + .ccm-banner__actions { display: flex; flex-wrap: wrap; @@ -123,9 +193,9 @@ } .ccm-banner__btn--primary { - background: #4a90d9; - border: 1px solid #4a90d9; - color: #fff; + background: var(--ccm-primary); + border: 1px solid var(--ccm-primary); + color: var(--ccm-primary-text); } .ccm-banner__btn--primary:hover { @@ -133,9 +203,9 @@ } .ccm-banner__btn--secondary { - background: rgba(255, 255, 255, 0.1); + background: var(--ccm-secondary-bg); border: 1px solid rgba(255, 255, 255, 0.28); - color: #fff; + color: var(--ccm-text); } .ccm-banner__btn--secondary:hover { @@ -145,7 +215,7 @@ .ccm-banner__btn--ghost { background: transparent; border: 1px solid rgba(255, 255, 255, 0.35); - color: #fff; + color: var(--ccm-text); } .ccm-banner__btn--ghost:hover { @@ -161,4 +231,9 @@ flex: 1 1 auto; text-align: center; } + + .ccm-banner__cookie-table { + display: block; + overflow-x: auto; + } } diff --git a/src/types.js b/src/types.js new file mode 100644 index 0000000..fa7baba --- /dev/null +++ b/src/types.js @@ -0,0 +1,46 @@ +/** + * @typedef {Object} UserConsent + * @property {boolean} analytics + * @property {boolean} marketing + */ + +/** + * @typedef {Object} RegionDefault + * @property {string[]} regions + * @property {"granted"|"denied"} [ad_storage] + * @property {"granted"|"denied"} [analytics_storage] + * @property {"granted"|"denied"} [ad_user_data] + * @property {"granted"|"denied"} [ad_personalization] + * @property {"granted"|"denied"} [functionality_storage] + * @property {"granted"|"denied"} [personalization_storage] + * @property {"granted"|"denied"} [security_storage] + */ + +/** + * @typedef {Object} ConsentConfig + * @property {string} [storageKey] + * @property {number} [waitForUpdateMs] + * @property {boolean} [adsDataRedaction] + * @property {boolean} [urlPassthrough] + * @property {RegionDefault[]} [regionDefaults] + * @property {boolean} [debug] + * @property {(error: { reason: string, detail?: unknown }) => void} [onStorageError] + */ + +/** + * @typedef {Object} ConsentResult + * @property {true} ok + * @property {UserConsent} consent + */ + +/** + * @typedef {Object} ConsentError + * @property {false} ok + * @property {"invalid"|"storage"} reason + */ + +/** + * @typedef {ConsentResult|ConsentError} PersistResult + */ + +export {}; diff --git a/vitest.config.js b/vitest.config.js new file mode 100644 index 0000000..558ed83 --- /dev/null +++ b/vitest.config.js @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["src/**/*.test.js"], + }, +});