diff --git a/.cursor/rules/ultracite.mdc b/.cursor/rules/ultracite.mdc
new file mode 100644
index 0000000..0b3bf93
--- /dev/null
+++ b/.cursor/rules/ultracite.mdc
@@ -0,0 +1,227 @@
+---
+description: Ultracite Rules - AI-Ready Formatter and Linter
+globs: "**/*.{ts,tsx,js,jsx,json,jsonc,html,vue,svelte,astro,css,yaml,yml,graphql,gql,md,mdx,grit}"
+alwaysApply: false
+---
+
+Avoid `accessKey` attr and distracting els
+No `aria-hidden="true"` on focusable els
+No ARIA roles, states, props on unsupported els
+Use `scope` prop only on `
` els
+No non-interactive ARIA roles on interactive els
+Label els need text and associated input
+No event handlers on non-interactive els
+No interactive ARIA roles on non-interactive els
+No `tabIndex` on non-interactive els
+No positive integers on `tabIndex` prop
+No `image`, `picture`, or `photo` in img alt props
+No explicit role matching implicit role
+Valid role attrs on static, visible els w/ click handlers
+Use `title` el for `svg` els
+Provide meaningful alt text for all els requiring it
+Anchors need accessible content
+Assign `tabIndex` to non-interactive els w/ `aria-activedescendant`
+Include all required ARIA attrs for els w/ ARIA roles
+Use valid ARIA props for the el's role
+Use `type` attr on `button` els
+Make els w/ interactive roles and handlers focusable
+Heading els need accessible content
+Add `lang` attr to `html` el
+Use `title` attr on `iframe` els
+Pair `onClick` w/ `onKeyUp`, `onKeyDown`, or `onKeyPress`
+Pair `onMouseOver`/`onMouseOut` w/ `onFocus`/`onBlur`
+Add caption tracks to audio and video els
+Use semantic els vs role attrs
+All anchors must be valid and navigable
+Use valid, non-abstract ARIA props, roles, states, and values
+Use valid values for `autocomplete` attr
+Use correct ISO language codes in `lang` attr
+Include generic font family in font families
+No consecutive spaces in regex literals
+Avoid `arguments`, comma op, and primitive type aliases
+No empty type params in type aliases and interfaces
+Keep fns under Cognitive Complexity limit
+Limit nesting depth of `describe()` in tests
+No unnecessary boolean casts or callbacks on `flatMap`
+Use `for...of` vs `Array.forEach`
+No classes w/ only static members
+No `this` and `super` in static contexts
+No unnecessary catch clauses, ctors, `continue`, escape sequences in regex literals, fragments, labels, or nested blocks
+No empty exports
+No renaming imports, exports, or destructured assignments to same name
+No unnecessary string/template literal concatenation or useless cases in switch stmts, `this` aliasing, or `String.raw` without escape sequences
+Use simpler alternatives to ternary ops if possible
+No `any` or `unknown` as type constraints or initializing vars to `undefined`
+Avoid `void` op
+Use arrow fns vs function exprs
+Use `Date.now()` for milliseconds since Unix Epoch
+Use `.flatMap()` vs `map().flat()`
+Use `indexOf`/`lastIndexOf` vs `findIndex`/`findLastIndex` for simple lookups
+Use literal property access vs computed property access
+Use binary, octal, or hex literals vs `parseInt()`
+Use concise optional chains vs chained logical exprs
+Use regex literals vs `RegExp` ctor
+Use base 10 or underscore separators for number literal object member names
+Remove redundant terms from logical exprs
+Use `while` loops vs `for` loops if initializer and update aren't needed
+No reassigning `const` vars or constant exprs in conditions
+No `Math.min`/`Math.max` to clamp values where result is constant
+No return values from ctors or setters
+No empty character classes in regex literals or destructuring patterns
+No `__dirname` and `__filename` in global scope
+No calling global object props as fns or declaring fns and `var` accessible outside their block
+Instantiate builtins correctly
+Use `super()` correctly in classes
+Use standard direction values for linear gradient fns
+Use valid named grid areas in CSS Grid Layouts
+Use `@import` at-rules in valid positions
+No vars and params before their decl
+Include `var` fn for CSS vars
+No `\8` and `\9` escape sequences in strings
+No literal numbers that lose precision, configured els, or assigning where both sides are same
+Compare string case modifications w/ compliant values
+No lexical decls in switch clauses or undeclared vars
+No unknown CSS value fns, media feature names, props, pseudo-class/pseudo-element selectors, type selectors, or units
+No unmatchable An+B selectors or unreachable code
+Call `super()` exactly once before accessing `this` in ctors
+No control flow stmts in `finally` blocks
+No optional chaining where `undefined` is not allowed
+No unused fn params, imports, labels, private class members, or vars
+No return values from fns w/ return type `void`
+Specify all dependencies correctly in React hooks and names for GraphQL operations
+Call React hooks from top level of component fns
+Use `isNaN()` when checking for NaN
+Use `{ type: "json" }` for JSON module imports
+Use radix arg w/ `parseInt()`
+Start JSDoc comment lines w/ single asterisk
+Move `for` loop counters in right direction
+Compare `typeof` exprs to valid values
+Include `yield` in generator fns
+No importing deprecated exports, duplicate dependencies, or Promises where they're likely a mistake
+No non-null assertions after optional chaining or shadowing vars from outer scope
+No expr stmts that aren't fn calls or assignments or useless `undefined`
+Add `href` attr to `` els and `width`/`height` attrs to ` ` els
+Use consistent arrow fn bodies and either `interface` or `type` consistently
+Specify deletion date w/ `@deprecated` directive
+Make switch-case stmts exhaustive and limit number of fn params
+Sort CSS utility classes
+No spread syntax on accumulators, barrel files, `delete` op, dynamic namespace import access, namespace imports, or duplicate polyfills from Polyfill.io
+Use `preconnect` attr w/ Google Fonts
+Declare regex literals at top level
+Add `rel="noopener"` when using `target="_blank"`
+No dangerous JSX props
+No both `children` and `dangerouslySetInnerHTML` props
+No global `eval()`
+No callbacks in async tests and hooks, TS enums, exporting imported vars, type annotations for vars initialized w/ literals, magic numbers without named constants, or TS namespaces
+No negating `if` conditions when there's an `else` clause, nested ternary exprs, non-null assertions (`!`), reassigning fn params, parameter props in class ctors, specified global var names, importing specified modules, or specified user-defined types
+No constants where value is upper-case version of name, template literals without interpolation or special chars, `else` blocks when `if` block breaks early, yoda exprs, or `Array` ctors
+Use `String.slice()` vs `String.substr()` and `String.substring()`
+Use `as const` vs literal type annotations and `at()` vs integer index access
+Follow curly brace conventions
+Use `else if` vs nested `if` in `else` clauses and single `if` vs nested `if` clauses
+Use `T[]` vs `Array`
+Use `new` for all builtins except `String`, `Number`, and `Boolean`
+Use consistent accessibility modifiers on class props and methods
+Declare object literals consistently
+Use `const` for vars only assigned once
+Put default and optional fn params last
+Include `default` clause in switch stmts
+Specify reason arg w/ `@deprecated` directive
+Explicitly initialize each enum member value
+Use `**` op vs `Math.pow`
+Use `export type` and `import type` for types
+Use kebab-case, ASCII filenames
+Use `for...of` vs `for` loops w/ array index access
+Use `<>...>` vs `...`
+Capitalize all enum values
+Place getters and setters for same prop adjacent
+Use literal values for all enum members
+Use `node:assert/strict` vs `node:assert`
+Use `node:` protocol for Node.js builtin modules
+Use `Number` props vs global ones
+Use numeric separators in numeric literals
+Use object spread vs `Object.assign()` for new objects
+Mark members `readonly` if never modified outside ctor
+No extra closing tags for comps without children
+Use assignment op shorthand
+Use fn types vs object types w/ call signatures
+Add description param to `Symbol()`
+Use template literals vs string concatenation
+Use `new` when throwing an error
+No throwing non-`Error` values
+Use `String.trimStart()`/`String.trimEnd()` vs `String.trimLeft()`/`String.trimRight()`
+No overload signatures that can be unified
+No lower specificity selectors after higher specificity selectors
+No `@value` rule in CSS modules
+No `alert`, `confirm`, and `prompt`
+Use standard constants vs approximated literals
+No assigning in exprs
+No async fns as Promise executors
+No `!` pattern in first position of `files.includes`
+No bitwise ops
+No reassigning exceptions in catch clauses
+No reassigning class members
+No inserting comments as text nodes
+No comparing against `-0`
+No labeled stmts that aren't loops
+No `void` type outside generic or return types
+No `console`
+No TS const enums
+No exprs where op doesn't affect value
+No control chars in regex literals
+No `debugger`
+No assigning directly to `document.cookie`
+Use `===` and `!==`
+No duplicate `@import` rules, case labels, class members, custom props, conditions in if-else-if chains, GraphQL fields, font family names, object keys, fn param names, decl block props, keyframe selectors, or describe hooks
+No empty CSS blocks, block stmts, static blocks, or interfaces
+No letting vars evolve into `any` type through reassignments
+No `any` type
+No `export` or `module.exports` in test files
+No misusing non-null assertion op (`!`)
+No fallthrough in switch clauses
+No focused or disabled tests
+No reassigning fn decls
+No assigning to native objects and read-only global vars
+Use `Number.isFinite` and `Number.isNaN` vs global `isFinite` and `isNaN`
+No implicit `any` type on var decls
+No assigning to imported bindings
+No `!important` within keyframe decls
+No irregular whitespace chars
+No labels that share name w/ var
+No chars made w/ multiple code points in char classes
+Use `new` and `constructor` properly
+Place assertion fns inside `it()` fn calls
+No shorthand assign when var appears on both sides
+No octal escape sequences in strings
+No `Object.prototype` builtins directly
+No `quickfix.biome` in editor settings
+No redeclaring vars, fns, classes, and types in same scope
+No redundant `use strict`
+No comparing where both sides are same
+No shadowing restricted names
+No shorthand props that override related longhand props
+No sparse arrays
+No template literal placeholder syntax in regular strings
+No `then` prop
+No `@ts-ignore` directive
+No `let` or `var` vars that are read but never assigned
+No unknown at-rules
+No merging interface and class decls unsafely
+No unsafe negation (`!`)
+No unnecessary escapes in strings or useless backreferences in regex literals
+No `var`
+No `with` stmts
+No separating overload signatures
+Use `await` in async fns
+Use correct syntax for ignoring folders in config
+Put default clauses in switch stmts last
+Pass message value when creating built-in errors
+Return value from get methods
+Use recommended display strategy w/ Google Fonts
+Include `if` stmt in for-in loops
+Use `Array.isArray()` vs `instanceof Array`
+Return consistent values in iterable callbacks
+Use `namespace` keyword vs `module` keyword
+Use digits arg w/ `Number#toFixed()`
+Use static `Response` methods vs `new Response()`
+Use `use strict` directive in script files
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 3a70dd7..4460cdc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,6 +14,7 @@ node_modules
# Results
results.json
+benchmarks/**/traces
# Testing
coverage
@@ -42,4 +43,4 @@ yarn-error.log*
test-results
-playwright-report
\ No newline at end of file
+.pnpm-store
diff --git a/METHODOLOGY.md b/METHODOLOGY.md
new file mode 100644
index 0000000..64a7d39
--- /dev/null
+++ b/METHODOLOGY.md
@@ -0,0 +1,290 @@
+# Benchmark Methodology
+
+This document explains how CookieBench measures and evaluates cookie banner performance. Our methodology prioritizes transparency, reproducibility, and real-world user experience.
+
+## Introduction
+
+CookieBench measures the performance impact of cookie consent solutions on web applications. We focus on metrics that directly impact user experience, following industry standards like Core Web Vitals and Lighthouse.
+
+## Measurement Approach
+
+### Dual Banner Timing Modes (DOM Presence vs User-Visible)
+
+Every benchmark run collects **both** timing modes for the cookie banner:
+
+**DOM presence time** (technical render):
+
+- Measures when the banner element first appears in the DOM
+- Recorded when the element has dimensions (`width > 0`, `height > 0`) and is not hidden
+- Exposed in results as `domPresenceTime` (alias: `bannerRenderTime` / `renderStart`)
+- Tracked for reference and displayed alongside user-visible time; **not used for scoring**
+
+**User-visible time** (opacity-threshold visibility):
+
+- Measures when the banner becomes visible to users (opacity > 0.5)
+- Uses opacity threshold of **0.5** to account for CSS animations and transitions
+- Exposed in results as `userVisibleTime` (alias: `bannerVisibilityTime` / `visibilityTime`)
+- **This metric is used for scoring and primary comparisons** by default
+
+**Why This Distinction Matters**:
+
+- A banner that renders instantly but fades in slowly should score differently than one that renders slower but is immediately visible
+- CSS animations can delay user-perceived visibility even after technical rendering
+- Measuring visibility aligns with Core Web Vitals' focus on user experience
+
+### Opacity Threshold
+
+We use an opacity threshold of **0.5** (50% opacity) to determine visibility:
+
+- An element with opacity ≤ 0.5 is considered not visible to users
+- An element with opacity > 0.5 is considered visible
+- This accounts for CSS fade-in animations that gradually reveal the banner
+- This threshold balances technical accuracy with user perception
+
+### Measurement Baseline
+
+All timing measurements start from `navigationStart`:
+
+- Includes Time to First Byte (TTFB)
+- Includes server response time
+- Includes network latency
+- Aligns with Core Web Vitals measurement standards
+- Represents total time from page load initiation to metric completion
+
+This means results reflect the complete user experience, including server-side performance and network conditions.
+
+## Network Conditions
+
+### Current Implementation
+
+Currently, benchmarks run on **localhost** with **no network throttling**:
+
+- Ideal network conditions (~9-35ms TTFB)
+- No bandwidth limitations
+- No latency simulation
+- Results reflect optimal performance
+
+**Impact on Results**:
+
+- Results represent best-case performance
+- Real-world performance will vary based on network conditions
+- Differences between implementations are isolated from network variability
+- Fair comparison when all implementations tested under same conditions
+
+### Future Considerations
+
+We are evaluating network throttling options:
+
+- **Cable Profile** (5 Mbps, 28ms latency): Minimal impact, maintains speed
+- **Fast 3G** (1.6 Mbps, 562ms latency): More realistic, significantly impacts TTFB
+- **Slow 4G**: Balanced approach for modern connections
+
+Any future changes to network conditions will be clearly documented and versioned.
+
+## Metrics Tracked
+
+### Primary Metrics (Used for Scoring)
+
+**User-visible time** (used for scoring):
+
+- Time from `navigationStart` until banner opacity > 0.5
+- Primary metric for UX scoring; scoring is based on this value, not DOM presence time
+- Accounts for CSS animations
+- Measured in milliseconds
+- Both DOM presence and user-visible times are collected each run and shown in CLI results
+
+**Banner Interactive Time**:
+
+- Time from `navigationStart` until banner buttons become clickable
+- Measures when users can actually interact with the banner
+- Uses `offsetParent` check to verify clickability
+
+**Layout Shift Impact**:
+
+- Cumulative Layout Shift (CLS) caused by banner appearance
+- Measures visual stability impact
+- Lower is better (0 = no layout shift)
+
+**Viewport Coverage**:
+
+- Percentage of viewport covered by banner
+- Calculated as: `(visibleArea / viewportArea) * 100`
+- Accounts for partial visibility (banners extending beyond viewport)
+
+**Network Impact**:
+
+- Total size of banner-related network requests
+- Number of network requests
+- Download time for banner resources
+
+### Secondary Metrics (Tracked for Reference)
+
+**Banner Render Time**:
+
+- Technical render time (when element appears in DOM)
+- Not used for scoring
+- Available for technical analysis
+
+**Banner Hydration Time**:
+
+- Time from render to interactive: `interactiveTime - renderTime`
+- Measures JavaScript execution and event binding time
+- Useful for understanding banner implementation performance
+
+## Scoring Methodology
+
+### How Scores Are Calculated
+
+Our scoring system evaluates multiple categories:
+
+**Performance Score** (40% weight):
+
+- First Contentful Paint (FCP)
+- Largest Contentful Paint (LCP)
+- Cumulative Layout Shift (CLS)
+- Time to Interactive (TTI)
+- Total Blocking Time (TBT)
+- Time to First Byte (TTFB)
+
+**Bundle Strategy Score** (25% weight):
+
+- Bundle type (Bundled vs IIFE)
+- Third-party dependencies
+- Bundler type
+- TypeScript usage
+
+**Network Impact Score** (20% weight):
+
+- Total bundle size
+- Third-party size
+- Network requests count
+- Script load time
+
+**Transparency Score** (10% weight):
+
+- Open-source status
+- Company information
+- Tech stack transparency
+
+**User Experience Score** (5% weight):
+
+- Layout stability (CLS)
+- Banner render time (uses visibility time)
+- Viewport coverage
+
+### Banner Visibility Time Scoring
+
+Banner visibility time is scored within the User Experience category:
+
+- **≤ 25ms**: Excellent (35 points)
+- **≤ 50ms**: Very Good (25 points)
+- **≤ 100ms**: Good (15 points)
+- **≤ 200ms**: Fair (10 points)
+- **> 200ms**: Poor (5 points)
+
+**Note**: Scoring uses `bannerVisibilityTime` (opacity-based), not `bannerRenderTime` (technical). This ensures scores reflect actual user experience.
+
+## Reproducibility
+
+### Configuration Requirements
+
+Each benchmark requires a `config.json` file with:
+
+- Cookie banner selectors (CSS selectors to detect the banner)
+- Service hosts (domains used by the cookie service)
+- Tech stack information
+- Iteration count
+
+### Running Benchmarks
+
+```bash
+# Run benchmarks
+pnpm benchmark
+
+# View results
+pnpm results
+```
+
+### What Affects Results
+
+**Consistent Factors**:
+
+- Browser engine (Chromium)
+- Viewport size (1280x720)
+- Detection selectors
+- Measurement methodology
+
+**Variable Factors**:
+
+- Server performance (if testing SSR)
+- System load
+- Network conditions (currently localhost)
+
+**Best Practices**:
+
+- Run multiple iterations (default: 20)
+- Ensure consistent server state
+- Clear browser cache between runs
+- Use same environment for all comparisons
+
+## Limitations
+
+### Current Limitations
+
+1. **Network Conditions**:
+ - No throttling means results reflect optimal conditions
+ - Real-world performance will vary
+ - Future: Network throttling support
+
+2. **Measurement Window**:
+ - Some metrics collected over 1-second window
+ - CLS may need longer observation periods
+ - Future: Configurable observation windows
+
+3. **Banner Detection**:
+ - Requires accurate CSS selectors
+ - May miss dynamically loaded banners
+ - Detection timeout: 10 seconds
+
+4. **Animation Timing**:
+ - Opacity threshold may not account for all animation types
+ - Transform-based animations not measured
+ - Future: Enhanced animation detection
+
+### Future Improvements
+
+- Network throttling support
+- More granular animation detection
+- Configurable observation windows
+- Enhanced banner detection algorithms
+- Real-world performance simulation
+
+## Transparency Statement
+
+We believe in transparent benchmarking:
+
+- **Open Methodology**: This document explains exactly how we measure
+- **Open Source**: Benchmark code is available for review
+- **Reproducible**: Anyone can run the same benchmarks
+- **Honest Metrics**: We measure user-perceived visibility, not just technical render time
+- **Clear Limitations**: We document what we measure and what we don't
+
+## Industry Standards Alignment
+
+Our methodology aligns with:
+
+- **Core Web Vitals**: User-centric performance metrics
+- **Lighthouse**: Measurement approaches and thresholds
+- **WebPageTest**: Performance testing practices
+- **W3C Performance Timeline**: Standard timing APIs
+
+We measure what matters to users, not just what's technically possible.
+
+## Questions or Feedback
+
+If you have questions about our methodology or suggestions for improvement, please open an issue on our GitHub repository.
+
+---
+
+**Last Updated**: 2026-02-16
+**Version**: 2.0
diff --git a/README.md b/README.md
index a4e888f..ed923f7 100644
--- a/README.md
+++ b/README.md
@@ -6,6 +6,8 @@ A benchmarking tool for measuring the performance impact of cookie consent solut
This tool measures various performance metrics when loading web applications with different cookie consent solutions. It helps developers understand the performance implications of their cookie consent implementation choices.
+For detailed information about how benchmarks are measured, see [METHODOLOGY.md](./METHODOLOGY.md).
+
## Metrics Measured
### Core Web Vitals
@@ -17,7 +19,11 @@ This tool measures various performance metrics when loading web applications wit
### Additional Metrics
- **Total Time**: Complete page load time
- **Script Load Time**: Time taken to load and execute JavaScript
-- **Banner Render Time**: Time taken to render the cookie consent banner
+- **DOM presence time**: Technical render time (when element appears in DOM); collected every run
+- **User-visible time**: When banner is visible to users (opacity > 0.5); **used for scoring**; collected every run
+- **Banner Interactive Time**: Time until banner buttons become clickable
+
+Both DOM presence and user-visible timings are collected in every run and shown in results. Scoring uses **user-visible time** by default. See [METHODOLOGY.md](./METHODOLOGY.md) for details.
### Resource Size Metrics
- **Total Size**: Combined size of all resources
@@ -41,7 +47,8 @@ This tool measures various performance metrics when loading web applications wit
2. **Performance Metrics**:
- Metrics are collected over a 1-second window after page load
- Some metrics (like CLS) may need longer observation periods
- - Network conditions are not simulated
+ - Network conditions are not simulated (benchmarks run on localhost)
+ - See [METHODOLOGY.md](./METHODOLOGY.md) for detailed limitations and measurement approach
## Usage
@@ -93,7 +100,7 @@ Each benchmark implementation requires a `config.json` file with the following s
- `frameworks`: Array of frameworks used
- `bundler`: Build tool used (e.g., webpack, vite)
- `packageManager`: Package manager used (e.g., npm, pnpm)
- - `bundleType`: Output format (esm, iffe, cjs)
+ - `bundleType`: Output format (esm, cjs, iffe, bundled)
- `typescript`: Whether TypeScript is used
- **source**: Project source information
diff --git a/benchmarks/baseline/next-env.d.ts b/benchmarks/baseline/next-env.d.ts
index 1b3be08..9edff1c 100644
--- a/benchmarks/baseline/next-env.d.ts
+++ b/benchmarks/baseline/next-env.d.ts
@@ -1,5 +1,6 @@
///
///
+import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/benchmarks/baseline/package.json b/benchmarks/baseline/package.json
index 973edfd..640cb87 100644
--- a/benchmarks/baseline/package.json
+++ b/benchmarks/baseline/package.json
@@ -3,25 +3,26 @@
"version": "0.1.0",
"private": true,
"scripts": {
- "benchmark": "pnpm exec benchmark-cli benchmark",
- "build": "next build ",
+ "build": "next build",
"dev": "next dev --port 3000",
"fmt": "biome format . --write",
"lint": "biome lint .",
"start": "next start"
},
"dependencies": {
- "next": "16.0.7",
- "react": "^19.2.1",
- "react-dom": "^19.2.1"
+ "next": "catalog:",
+ "react": "catalog:",
+ "react-dom": "catalog:"
},
"devDependencies": {
"@cookiebench/benchmark-schema": "workspace:*",
- "@cookiebench/cli": "workspace:*",
"@cookiebench/ts-config": "workspace:*",
- "@types/node": "^24.10.1",
- "@types/react": "^19.2.7",
- "@types/react-dom": "^19.2.3",
- "typescript": "^5.9.3"
+ "@types/node": "catalog:",
+ "@types/react": "catalog:",
+ "@types/react-dom": "catalog:",
+ "typescript": "catalog:"
+ },
+ "engines": {
+ "node": ">=20.9.0"
}
}
diff --git a/benchmarks/with-c15t-nextjs/app/favicon.ico b/benchmarks/c15t-nextjs/app/favicon.ico
similarity index 100%
rename from benchmarks/with-c15t-nextjs/app/favicon.ico
rename to benchmarks/c15t-nextjs/app/favicon.ico
diff --git a/benchmarks/with-c15t-nextjs/app/layout.tsx b/benchmarks/c15t-nextjs/app/layout.tsx
similarity index 100%
rename from benchmarks/with-c15t-nextjs/app/layout.tsx
rename to benchmarks/c15t-nextjs/app/layout.tsx
diff --git a/benchmarks/with-c15t-nextjs/app/page.tsx b/benchmarks/c15t-nextjs/app/page.tsx
similarity index 100%
rename from benchmarks/with-c15t-nextjs/app/page.tsx
rename to benchmarks/c15t-nextjs/app/page.tsx
diff --git a/benchmarks/with-c15t-nextjs/config.json b/benchmarks/c15t-nextjs/config.json
similarity index 97%
rename from benchmarks/with-c15t-nextjs/config.json
rename to benchmarks/c15t-nextjs/config.json
index 9aabb6b..2735964 100644
--- a/benchmarks/with-c15t-nextjs/config.json
+++ b/benchmarks/c15t-nextjs/config.json
@@ -1,6 +1,6 @@
{
"$schema": "./node_modules/@cookiebench/benchmark-schema/schema.json",
- "name": "with-c15t-nextjs",
+ "name": "c15t-nextjs",
"iterations": 20,
"cookieBanner": {
"selectors": [
diff --git a/benchmarks/with-c15t-react/next-env.d.ts b/benchmarks/c15t-nextjs/next-env.d.ts
similarity index 85%
rename from benchmarks/with-c15t-react/next-env.d.ts
rename to benchmarks/c15t-nextjs/next-env.d.ts
index 1b3be08..9edff1c 100644
--- a/benchmarks/with-c15t-react/next-env.d.ts
+++ b/benchmarks/c15t-nextjs/next-env.d.ts
@@ -1,5 +1,6 @@
///
///
+import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/benchmarks/c15t-nextjs/next.config.ts b/benchmarks/c15t-nextjs/next.config.ts
new file mode 100644
index 0000000..dd8b328
--- /dev/null
+++ b/benchmarks/c15t-nextjs/next.config.ts
@@ -0,0 +1,15 @@
+import type { NextConfig } from "next";
+
+const nextConfig: NextConfig = {
+ images: {
+ localPatterns: [
+ {
+ pathname: "/**",
+ search: "",
+ },
+ ],
+ minimumCacheTTL: 60,
+ },
+};
+
+export default nextConfig;
diff --git a/benchmarks/with-cookie-control/package.json b/benchmarks/c15t-nextjs/package.json
similarity index 51%
rename from benchmarks/with-cookie-control/package.json
rename to benchmarks/c15t-nextjs/package.json
index d85d95f..10730e3 100644
--- a/benchmarks/with-cookie-control/package.json
+++ b/benchmarks/c15t-nextjs/package.json
@@ -1,8 +1,8 @@
{
- "name": "with-cookie-control",
+ "name": "c15t-nextjs",
+ "version": "0.1.0",
"private": true,
"scripts": {
- "benchmark": "pnpm exec benchmark-cli benchmark",
"build": "next build",
"dev": "next dev --port 3001",
"fmt": "biome format . --write",
@@ -10,17 +10,20 @@
"start": "next start"
},
"dependencies": {
- "next": "16.0.7",
- "react": "^19.2.1",
- "react-dom": "^19.2.1"
+ "@c15t/nextjs": "1.7.1",
+ "next": "catalog:",
+ "react": "catalog:",
+ "react-dom": "catalog:"
},
"devDependencies": {
"@cookiebench/benchmark-schema": "workspace:*",
- "@cookiebench/cli": "workspace:*",
"@cookiebench/ts-config": "workspace:*",
- "@types/node": "^24.10.1",
- "@types/react": "^19.2.7",
- "@types/react-dom": "^19.2.3",
- "typescript": "^5.9.3"
+ "@types/node": "catalog:",
+ "@types/react": "catalog:",
+ "@types/react-dom": "catalog:",
+ "typescript": "catalog:"
+ },
+ "engines": {
+ "node": ">=20.9.0"
}
}
diff --git a/benchmarks/with-c15t-nextjs/tsconfig.json b/benchmarks/c15t-nextjs/tsconfig.json
similarity index 100%
rename from benchmarks/with-c15t-nextjs/tsconfig.json
rename to benchmarks/c15t-nextjs/tsconfig.json
diff --git a/benchmarks/with-c15t-react/app/favicon.ico b/benchmarks/c15t-react/app/favicon.ico
similarity index 100%
rename from benchmarks/with-c15t-react/app/favicon.ico
rename to benchmarks/c15t-react/app/favicon.ico
diff --git a/benchmarks/with-c15t-react/app/layout.tsx b/benchmarks/c15t-react/app/layout.tsx
similarity index 100%
rename from benchmarks/with-c15t-react/app/layout.tsx
rename to benchmarks/c15t-react/app/layout.tsx
diff --git a/benchmarks/with-c15t-react/app/page.tsx b/benchmarks/c15t-react/app/page.tsx
similarity index 100%
rename from benchmarks/with-c15t-react/app/page.tsx
rename to benchmarks/c15t-react/app/page.tsx
diff --git a/benchmarks/with-c15t-react/config.json b/benchmarks/c15t-react/config.json
similarity index 87%
rename from benchmarks/with-c15t-react/config.json
rename to benchmarks/c15t-react/config.json
index b174fc7..445e69f 100644
--- a/benchmarks/with-c15t-react/config.json
+++ b/benchmarks/c15t-react/config.json
@@ -1,6 +1,6 @@
{
"$schema": "./node_modules/@cookiebench/benchmark-schema/schema.json",
- "name": "with-c15t-react",
+ "name": "c15t-react",
"iterations": 20,
"cookieBanner": {
"selectors": [
@@ -10,7 +10,11 @@
".consent-banner",
"[data-consent-banner]"
],
- "serviceHosts": ["c15t.com", "consent.io"],
+ "serviceHosts": [
+ "c15t.com",
+ "consent.io",
+ "consent-io-europe-benchmarks.c15t.dev"
+ ],
"waitForVisibility": true,
"measureViewportCoverage": true,
"expectedLayoutShift": false,
@@ -28,7 +32,7 @@
"github": "github.com/c15t/c15t",
"isOpenSource": true,
"license": "GPL-3.0-only",
- "npm": "@c15t/nextjs",
+ "npm": "@c15t/react@1.7.1",
"website": "https://www.c15t.com"
},
"includes": {
diff --git a/benchmarks/with-c15t-nextjs/next-env.d.ts b/benchmarks/c15t-react/next-env.d.ts
similarity index 85%
rename from benchmarks/with-c15t-nextjs/next-env.d.ts
rename to benchmarks/c15t-react/next-env.d.ts
index 1b3be08..9edff1c 100644
--- a/benchmarks/with-c15t-nextjs/next-env.d.ts
+++ b/benchmarks/c15t-react/next-env.d.ts
@@ -1,5 +1,6 @@
///
///
+import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/benchmarks/with-c15t-nextjs/next.config.ts b/benchmarks/c15t-react/next.config.ts
similarity index 100%
rename from benchmarks/with-c15t-nextjs/next.config.ts
rename to benchmarks/c15t-react/next.config.ts
diff --git a/benchmarks/c15t-react/package.json b/benchmarks/c15t-react/package.json
new file mode 100644
index 0000000..9052679
--- /dev/null
+++ b/benchmarks/c15t-react/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "c15t-react",
+ "private": true,
+ "scripts": {
+ "build": "next build",
+ "dev": "next dev --port 3003",
+ "fmt": "biome format . --write",
+ "lint": "biome lint .",
+ "start": "next start --port 3003"
+ },
+ "dependencies": {
+ "@c15t/react": "1.7.1",
+ "next": "catalog:",
+ "react": "catalog:",
+ "react-dom": "catalog:"
+ },
+ "devDependencies": {
+ "@cookiebench/benchmark-schema": "workspace:*",
+ "@cookiebench/ts-config": "workspace:*",
+ "@types/node": "catalog:",
+ "@types/react": "catalog:",
+ "@types/react-dom": "catalog:",
+ "typescript": "catalog:"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ }
+}
diff --git a/benchmarks/with-c15t-react/tsconfig.json b/benchmarks/c15t-react/tsconfig.json
similarity index 100%
rename from benchmarks/with-c15t-react/tsconfig.json
rename to benchmarks/c15t-react/tsconfig.json
diff --git a/benchmarks/with-cookie-control/app/favicon.ico b/benchmarks/cookie-control/app/favicon.ico
similarity index 100%
rename from benchmarks/with-cookie-control/app/favicon.ico
rename to benchmarks/cookie-control/app/favicon.ico
diff --git a/benchmarks/with-cookie-control/app/layout.tsx b/benchmarks/cookie-control/app/layout.tsx
similarity index 83%
rename from benchmarks/with-cookie-control/app/layout.tsx
rename to benchmarks/cookie-control/app/layout.tsx
index 505cff4..beb311a 100644
--- a/benchmarks/with-cookie-control/app/layout.tsx
+++ b/benchmarks/cookie-control/app/layout.tsx
@@ -1,8 +1,8 @@
-import type { Metadata } from 'next';
-import type { ReactNode } from 'react';
+import type { Metadata } from "next";
+import type { ReactNode } from "react";
export const metadata: Metadata = {
- title: 'benchmark',
+ title: "benchmark",
};
export default function RootLayout({
@@ -19,7 +19,7 @@ export default function RootLayout({
type="text/javascript"
/>
{children}
diff --git a/benchmarks/with-cookie-control/app/page.tsx b/benchmarks/cookie-control/app/page.tsx
similarity index 100%
rename from benchmarks/with-cookie-control/app/page.tsx
rename to benchmarks/cookie-control/app/page.tsx
diff --git a/benchmarks/cookie-control/config.json b/benchmarks/cookie-control/config.json
new file mode 100644
index 0000000..415caa4
--- /dev/null
+++ b/benchmarks/cookie-control/config.json
@@ -0,0 +1,40 @@
+{
+ "$schema": "./node_modules/@cookiebench/benchmark-schema/schema.json",
+ "name": "cookie-control",
+ "id": "cookie-control",
+ "iterations": 20,
+ "cookieBanner": {
+ "selectors": [".ccc-module--slideout"],
+ "serviceHosts": ["civiccomputing.com"],
+ "waitForVisibility": true,
+ "measureViewportCoverage": true,
+ "expectedLayoutShift": true,
+ "serviceName": "Cookie Control"
+ },
+ "internationalization": {
+ "detection": "none",
+ "stringLoading": "none"
+ },
+ "techStack": {
+ "bundler": "unknown",
+ "bundleType": "iffe",
+ "frameworks": [],
+ "languages": ["javascript"],
+ "packageManager": "unknown",
+ "typescript": false
+ },
+ "source": {
+ "isOpenSource": false,
+ "license": "proprietary",
+ "website": "https://www.civicuk.com/cookie-control/"
+ },
+ "company": {
+ "name": "Cookie Control",
+ "website": "https://www.civicuk.com/cookie-control/",
+ "avatar": "https://www.civicuk.com/cookie-control/images/favicon.png"
+ },
+ "includes": {
+ "backend": ["proprietary"],
+ "components": ["javascript"]
+ }
+}
diff --git a/benchmarks/with-cookie-control/next-env.d.ts b/benchmarks/cookie-control/next-env.d.ts
similarity index 85%
rename from benchmarks/with-cookie-control/next-env.d.ts
rename to benchmarks/cookie-control/next-env.d.ts
index 1b3be08..9edff1c 100644
--- a/benchmarks/with-cookie-control/next-env.d.ts
+++ b/benchmarks/cookie-control/next-env.d.ts
@@ -1,5 +1,6 @@
///
///
+import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/benchmarks/with-c15t-react/next.config.ts b/benchmarks/cookie-control/next.config.ts
similarity index 100%
rename from benchmarks/with-c15t-react/next.config.ts
rename to benchmarks/cookie-control/next.config.ts
diff --git a/benchmarks/with-enzuzo/package.json b/benchmarks/cookie-control/package.json
similarity index 52%
rename from benchmarks/with-enzuzo/package.json
rename to benchmarks/cookie-control/package.json
index 6dfbb46..ba6e68a 100644
--- a/benchmarks/with-enzuzo/package.json
+++ b/benchmarks/cookie-control/package.json
@@ -1,8 +1,7 @@
{
- "name": "with-enzuzo",
+ "name": "cookie-control",
"private": true,
"scripts": {
- "benchmark": "pnpm exec benchmark-cli benchmark",
"build": "next build",
"dev": "next dev --port 3001",
"fmt": "biome format . --write",
@@ -10,17 +9,19 @@
"start": "next start"
},
"dependencies": {
- "next": "16.0.7",
- "react": "^19.2.1",
- "react-dom": "^19.2.1"
+ "next": "catalog:",
+ "react": "catalog:",
+ "react-dom": "catalog:"
},
"devDependencies": {
"@cookiebench/benchmark-schema": "workspace:*",
- "@cookiebench/cli": "workspace:*",
"@cookiebench/ts-config": "workspace:*",
- "@types/node": "^24.10.1",
- "@types/react": "^19.2.7",
- "@types/react-dom": "^19.2.3",
- "typescript": "^5.9.3"
+ "@types/node": "catalog:",
+ "@types/react": "catalog:",
+ "@types/react-dom": "catalog:",
+ "typescript": "catalog:"
+ },
+ "engines": {
+ "node": ">=20.9.0"
}
}
diff --git a/benchmarks/with-cookie-yes/tsconfig.json b/benchmarks/cookie-control/tsconfig.json
similarity index 100%
rename from benchmarks/with-cookie-yes/tsconfig.json
rename to benchmarks/cookie-control/tsconfig.json
diff --git a/benchmarks/with-cookie-yes/app/favicon.ico b/benchmarks/cookie-yes/app/favicon.ico
similarity index 100%
rename from benchmarks/with-cookie-yes/app/favicon.ico
rename to benchmarks/cookie-yes/app/favicon.ico
diff --git a/benchmarks/with-cookie-yes/app/layout.tsx b/benchmarks/cookie-yes/app/layout.tsx
similarity index 100%
rename from benchmarks/with-cookie-yes/app/layout.tsx
rename to benchmarks/cookie-yes/app/layout.tsx
diff --git a/benchmarks/with-cookie-yes/app/page.tsx b/benchmarks/cookie-yes/app/page.tsx
similarity index 100%
rename from benchmarks/with-cookie-yes/app/page.tsx
rename to benchmarks/cookie-yes/app/page.tsx
diff --git a/benchmarks/with-cookie-yes/config.json b/benchmarks/cookie-yes/config.json
similarity index 92%
rename from benchmarks/with-cookie-yes/config.json
rename to benchmarks/cookie-yes/config.json
index b084c19..ac9958d 100644
--- a/benchmarks/with-cookie-yes/config.json
+++ b/benchmarks/cookie-yes/config.json
@@ -1,11 +1,11 @@
{
"$schema": "./node_modules/@cookiebench/benchmark-schema/schema.json",
- "name": "with-cookie-yes",
+ "name": "cookie-yes",
"id": "cookie-yes-banner",
"iterations": 20,
"remote": {
"enabled": true,
- "url": "https://benchmarks-with-cookie-yes.vercel.app"
+ "url": "https://benchmarks-cookie-yes.vercel.app"
},
"cookieBanner": {
"selectors": [".cky-consent-container"],
diff --git a/benchmarks/with-cookie-yes/next-env.d.ts b/benchmarks/cookie-yes/next-env.d.ts
similarity index 85%
rename from benchmarks/with-cookie-yes/next-env.d.ts
rename to benchmarks/cookie-yes/next-env.d.ts
index 1b3be08..9edff1c 100644
--- a/benchmarks/with-cookie-yes/next-env.d.ts
+++ b/benchmarks/cookie-yes/next-env.d.ts
@@ -1,5 +1,6 @@
///
///
+import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/benchmarks/with-cookie-yes/next.config.ts b/benchmarks/cookie-yes/next.config.ts
similarity index 100%
rename from benchmarks/with-cookie-yes/next.config.ts
rename to benchmarks/cookie-yes/next.config.ts
diff --git a/benchmarks/with-iubenda/package.json b/benchmarks/cookie-yes/package.json
similarity index 52%
rename from benchmarks/with-iubenda/package.json
rename to benchmarks/cookie-yes/package.json
index f0659d2..cc4a290 100644
--- a/benchmarks/with-iubenda/package.json
+++ b/benchmarks/cookie-yes/package.json
@@ -1,8 +1,7 @@
{
- "name": "with-iubenda",
+ "name": "cookie-yes",
"private": true,
"scripts": {
- "benchmark": "pnpm exec benchmark-cli benchmark",
"build": "next build",
"dev": "next dev --port 3001",
"fmt": "biome format . --write",
@@ -10,17 +9,19 @@
"start": "next start"
},
"dependencies": {
- "next": "16.0.7",
- "react": "^19.2.1",
- "react-dom": "^19.2.1"
+ "next": "catalog:",
+ "react": "catalog:",
+ "react-dom": "catalog:"
},
"devDependencies": {
"@cookiebench/benchmark-schema": "workspace:*",
- "@cookiebench/cli": "workspace:*",
"@cookiebench/ts-config": "workspace:*",
- "@types/node": "^24.10.1",
- "@types/react": "^19.2.7",
- "@types/react-dom": "^19.2.3",
- "typescript": "^5.9.3"
+ "@types/node": "catalog:",
+ "@types/react": "catalog:",
+ "@types/react-dom": "catalog:",
+ "typescript": "catalog:"
+ },
+ "engines": {
+ "node": ">=20.9.0"
}
}
diff --git a/benchmarks/with-didomi/tsconfig.json b/benchmarks/cookie-yes/tsconfig.json
similarity index 100%
rename from benchmarks/with-didomi/tsconfig.json
rename to benchmarks/cookie-yes/tsconfig.json
diff --git a/benchmarks/with-didomi/app/favicon.ico b/benchmarks/didomi/app/favicon.ico
similarity index 100%
rename from benchmarks/with-didomi/app/favicon.ico
rename to benchmarks/didomi/app/favicon.ico
diff --git a/benchmarks/didomi/app/layout.tsx b/benchmarks/didomi/app/layout.tsx
new file mode 100644
index 0000000..8674ead
--- /dev/null
+++ b/benchmarks/didomi/app/layout.tsx
@@ -0,0 +1,92 @@
+/** biome-ignore-all lint/suspicious/noConsole: its okay to show it working */
+"use client";
+
+import { DidomiSDK, type IDidomiObject } from "@didomi/react";
+import { type ReactNode, useCallback, useState } from "react";
+
+const DEBUG_DIDOMI =
+ process.env.NEXT_PUBLIC_DEBUG_DIDOMI === "true" ||
+ process.env.DEBUG_DIDOMI === "true";
+const TOKEN_VISIBLE_CHARS = 4;
+
+function redactToken(token: string): string {
+ if (!token) {
+ return "****";
+ }
+ const last4 = token.slice(-TOKEN_VISIBLE_CHARS);
+ return `****${last4}`;
+}
+
+export default function RootLayout({
+ children,
+}: Readonly<{
+ children: ReactNode;
+}>) {
+ const [didomiObject, setDidomiObject] = useState(null);
+
+ const onDidomiReady = useCallback((didomi: IDidomiObject) => {
+ setDidomiObject(didomi);
+ if (!DEBUG_DIDOMI) {
+ return;
+ }
+
+ console.log("Didomi ready", {
+ consentRequired: didomi.isConsentRequired(),
+ vendor1Consent: didomi.getUserConsentStatusForVendor(1)
+ ? "granted"
+ : "denied",
+ vendor1CookiesConsent: didomi.getUserConsentStatus("cookies", 1)
+ ? "granted"
+ : "denied",
+ });
+ }, []);
+
+ const onConsentChanged = useCallback(
+ (cwtToken: string) => {
+ if (!didomiObject) {
+ return;
+ }
+ if (!DEBUG_DIDOMI) {
+ return;
+ }
+
+ console.log("Didomi consent changed", {
+ cwtToken: redactToken(cwtToken),
+ consentRequired: didomiObject.isConsentRequired(),
+ vendor1Consent: didomiObject.getUserConsentStatusForVendor(1)
+ ? "granted"
+ : "denied",
+ vendor1CookiesConsent: didomiObject.getUserConsentStatus("cookies", 1)
+ ? "granted"
+ : "denied",
+ });
+ },
+ [didomiObject]
+ );
+
+ return (
+
+
+ {
+ if (DEBUG_DIDOMI) {
+ console.log("Didomi notice hidden");
+ }
+ }}
+ onNoticeShown={() => {
+ if (DEBUG_DIDOMI) {
+ console.log("Didomi notice shown");
+ }
+ }}
+ onReady={onDidomiReady}
+ />
+ {children}
+
+
+ );
+}
diff --git a/benchmarks/with-didomi/app/page.tsx b/benchmarks/didomi/app/page.tsx
similarity index 100%
rename from benchmarks/with-didomi/app/page.tsx
rename to benchmarks/didomi/app/page.tsx
diff --git a/benchmarks/with-didomi/config.json b/benchmarks/didomi/config.json
similarity index 97%
rename from benchmarks/with-didomi/config.json
rename to benchmarks/didomi/config.json
index c007ba3..185d5f5 100644
--- a/benchmarks/with-didomi/config.json
+++ b/benchmarks/didomi/config.json
@@ -1,6 +1,6 @@
{
"$schema": "./node_modules/@cookiebench/benchmark-schema/schema.json",
- "name": "with-didomi",
+ "name": "didomi",
"iterations": 20,
"cookieBanner": {
"selectors": [
diff --git a/benchmarks/didomi/next-env.d.ts b/benchmarks/didomi/next-env.d.ts
new file mode 100644
index 0000000..9edff1c
--- /dev/null
+++ b/benchmarks/didomi/next-env.d.ts
@@ -0,0 +1,6 @@
+///
+///
+import "./.next/types/routes.d.ts";
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/benchmarks/with-didomi/next.config.ts b/benchmarks/didomi/next.config.ts
similarity index 100%
rename from benchmarks/with-didomi/next.config.ts
rename to benchmarks/didomi/next.config.ts
diff --git a/benchmarks/with-didomi/package.json b/benchmarks/didomi/package.json
similarity index 56%
rename from benchmarks/with-didomi/package.json
rename to benchmarks/didomi/package.json
index a5873b8..23ed359 100644
--- a/benchmarks/with-didomi/package.json
+++ b/benchmarks/didomi/package.json
@@ -1,8 +1,7 @@
{
- "name": "with-dodomi",
+ "name": "didomi",
"private": true,
"scripts": {
- "benchmark": "pnpm exec benchmark-cli benchmark",
"build": "next build",
"dev": "next dev",
"fmt": "biome format . --write",
@@ -11,16 +10,19 @@
},
"dependencies": {
"@didomi/react": "^1.8.8",
- "next": "16.0.7",
- "react": "^19.2.1",
- "react-dom": "^19.2.1"
+ "next": "catalog:",
+ "react": "catalog:",
+ "react-dom": "catalog:"
},
"devDependencies": {
"@cookiebench/benchmark-schema": "workspace:*",
"@cookiebench/ts-config": "workspace:*",
- "@types/node": "^24.10.1",
- "@types/react": "^19.2.7",
- "@types/react-dom": "^19.2.3",
- "typescript": "^5.9.3"
+ "@types/node": "catalog:",
+ "@types/react": "catalog:",
+ "@types/react-dom": "catalog:",
+ "typescript": "catalog:"
+ },
+ "engines": {
+ "node": ">=20.9.0"
}
}
diff --git a/benchmarks/with-iubenda/tsconfig.json b/benchmarks/didomi/tsconfig.json
similarity index 100%
rename from benchmarks/with-iubenda/tsconfig.json
rename to benchmarks/didomi/tsconfig.json
diff --git a/benchmarks/with-enzuzo/app/favicon.ico b/benchmarks/enzuzo/app/favicon.ico
similarity index 100%
rename from benchmarks/with-enzuzo/app/favicon.ico
rename to benchmarks/enzuzo/app/favicon.ico
diff --git a/benchmarks/with-osano/app/layout.tsx b/benchmarks/enzuzo/app/layout.tsx
similarity index 67%
rename from benchmarks/with-osano/app/layout.tsx
rename to benchmarks/enzuzo/app/layout.tsx
index deb9cc6..dc03277 100644
--- a/benchmarks/with-osano/app/layout.tsx
+++ b/benchmarks/enzuzo/app/layout.tsx
@@ -13,7 +13,11 @@ export default function RootLayout({
return (
-
+ {/* Cookie Control Script */}
+
{children}
diff --git a/benchmarks/with-enzuzo/app/page.tsx b/benchmarks/enzuzo/app/page.tsx
similarity index 100%
rename from benchmarks/with-enzuzo/app/page.tsx
rename to benchmarks/enzuzo/app/page.tsx
diff --git a/benchmarks/enzuzo/config.json b/benchmarks/enzuzo/config.json
new file mode 100644
index 0000000..fac2212
--- /dev/null
+++ b/benchmarks/enzuzo/config.json
@@ -0,0 +1,40 @@
+{
+ "$schema": "./node_modules/@cookiebench/benchmark-schema/schema.json",
+ "name": "enzuzo",
+ "id": "enzuzo",
+ "iterations": 20,
+ "cookieBanner": {
+ "selectors": [".enzuzo-cookiebanner-container"],
+ "serviceHosts": ["app.enzuzo.com"],
+ "waitForVisibility": true,
+ "measureViewportCoverage": true,
+ "expectedLayoutShift": true,
+ "serviceName": "Enzuzo"
+ },
+ "internationalization": {
+ "detection": "none",
+ "stringLoading": "none"
+ },
+ "techStack": {
+ "bundler": "unknown",
+ "bundleType": "iffe",
+ "frameworks": [],
+ "languages": ["javascript"],
+ "packageManager": "unknown",
+ "typescript": false
+ },
+ "source": {
+ "isOpenSource": false,
+ "license": "proprietary",
+ "website": "https://www.enzuzo.com/"
+ },
+ "company": {
+ "name": "Enzuzo",
+ "website": "https://www.enzuzo.com/",
+ "avatar": "https://pbs.twimg.com/profile_images/1519664904562786305/h7G0tW79_400x400.jpg"
+ },
+ "includes": {
+ "backend": ["proprietary"],
+ "components": ["javascript"]
+ }
+}
diff --git a/benchmarks/enzuzo/next-env.d.ts b/benchmarks/enzuzo/next-env.d.ts
new file mode 100644
index 0000000..9edff1c
--- /dev/null
+++ b/benchmarks/enzuzo/next-env.d.ts
@@ -0,0 +1,6 @@
+///
+///
+import "./.next/types/routes.d.ts";
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/benchmarks/with-iubenda/next.config.ts b/benchmarks/enzuzo/next.config.ts
similarity index 100%
rename from benchmarks/with-iubenda/next.config.ts
rename to benchmarks/enzuzo/next.config.ts
diff --git a/benchmarks/enzuzo/package.json b/benchmarks/enzuzo/package.json
new file mode 100644
index 0000000..ffbd6a5
--- /dev/null
+++ b/benchmarks/enzuzo/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "enzuzo",
+ "private": true,
+ "scripts": {
+ "build": "next build",
+ "dev": "next dev --port 3002",
+ "fmt": "biome format . --write",
+ "lint": "biome lint .",
+ "start": "next start"
+ },
+ "dependencies": {
+ "next": "catalog:",
+ "react": "catalog:",
+ "react-dom": "catalog:"
+ },
+ "devDependencies": {
+ "@cookiebench/benchmark-schema": "workspace:*",
+ "@cookiebench/ts-config": "workspace:*",
+ "@types/node": "catalog:",
+ "@types/react": "catalog:",
+ "@types/react-dom": "catalog:",
+ "typescript": "catalog:"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ }
+}
diff --git a/benchmarks/with-ketch/tsconfig.json b/benchmarks/enzuzo/tsconfig.json
similarity index 100%
rename from benchmarks/with-ketch/tsconfig.json
rename to benchmarks/enzuzo/tsconfig.json
diff --git a/benchmarks/with-iubenda/app/favicon.ico b/benchmarks/iubenda/app/favicon.ico
similarity index 100%
rename from benchmarks/with-iubenda/app/favicon.ico
rename to benchmarks/iubenda/app/favicon.ico
diff --git a/benchmarks/with-iubenda/app/layout.tsx b/benchmarks/iubenda/app/layout.tsx
similarity index 96%
rename from benchmarks/with-iubenda/app/layout.tsx
rename to benchmarks/iubenda/app/layout.tsx
index 66fa923..3953c20 100644
--- a/benchmarks/with-iubenda/app/layout.tsx
+++ b/benchmarks/iubenda/app/layout.tsx
@@ -15,6 +15,7 @@ export default function RootLayout({
{/* Iubenda Cookie Banner + Configuration */}
{children}
diff --git a/benchmarks/with-iubenda/app/page.tsx b/benchmarks/iubenda/app/page.tsx
similarity index 100%
rename from benchmarks/with-iubenda/app/page.tsx
rename to benchmarks/iubenda/app/page.tsx
diff --git a/benchmarks/with-iubenda/config.json b/benchmarks/iubenda/config.json
similarity index 97%
rename from benchmarks/with-iubenda/config.json
rename to benchmarks/iubenda/config.json
index 8ee60b6..2e2e929 100644
--- a/benchmarks/with-iubenda/config.json
+++ b/benchmarks/iubenda/config.json
@@ -1,6 +1,6 @@
{
"$schema": "./node_modules/@cookiebench/benchmark-schema/schema.json",
- "name": "with-iubenda",
+ "name": "iubenda",
"id": "iubenda-cs-banner",
"iterations": 20,
"cookieBanner": {
diff --git a/benchmarks/iubenda/next-env.d.ts b/benchmarks/iubenda/next-env.d.ts
new file mode 100644
index 0000000..9edff1c
--- /dev/null
+++ b/benchmarks/iubenda/next-env.d.ts
@@ -0,0 +1,6 @@
+///
+///
+import "./.next/types/routes.d.ts";
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/benchmarks/with-ketch/next.config.ts b/benchmarks/iubenda/next.config.ts
similarity index 100%
rename from benchmarks/with-ketch/next.config.ts
rename to benchmarks/iubenda/next.config.ts
diff --git a/benchmarks/with-cookie-yes/package.json b/benchmarks/iubenda/package.json
similarity index 51%
rename from benchmarks/with-cookie-yes/package.json
rename to benchmarks/iubenda/package.json
index a0f35e6..46933ac 100644
--- a/benchmarks/with-cookie-yes/package.json
+++ b/benchmarks/iubenda/package.json
@@ -1,8 +1,7 @@
{
- "name": "with-cookie-yes",
+ "name": "iubenda",
"private": true,
"scripts": {
- "benchmark": "pnpm exec benchmark-cli benchmark",
"build": "next build",
"dev": "next dev --port 3001",
"fmt": "biome format . --write",
@@ -10,17 +9,19 @@
"start": "next start"
},
"dependencies": {
- "next": "16.0.7",
- "react": "^19.2.1",
- "react-dom": "^19.2.1"
+ "next": "catalog:",
+ "react": "catalog:",
+ "react-dom": "catalog:"
},
"devDependencies": {
"@cookiebench/benchmark-schema": "workspace:*",
- "@cookiebench/cli": "workspace:*",
"@cookiebench/ts-config": "workspace:*",
- "@types/node": "^24.10.1",
- "@types/react": "^19.2.7",
- "@types/react-dom": "^19.2.3",
- "typescript": "^5.9.3"
+ "@types/node": "catalog:",
+ "@types/react": "catalog:",
+ "@types/react-dom": "catalog:",
+ "typescript": "catalog:"
+ },
+ "engines": {
+ "node": ">=20.9.0"
}
}
diff --git a/benchmarks/with-onetrust/tsconfig.json b/benchmarks/iubenda/tsconfig.json
similarity index 100%
rename from benchmarks/with-onetrust/tsconfig.json
rename to benchmarks/iubenda/tsconfig.json
diff --git a/benchmarks/with-ketch/app/favicon.ico b/benchmarks/ketch/app/favicon.ico
similarity index 100%
rename from benchmarks/with-ketch/app/favicon.ico
rename to benchmarks/ketch/app/favicon.ico
diff --git a/benchmarks/with-ketch/app/layout.tsx b/benchmarks/ketch/app/layout.tsx
similarity index 100%
rename from benchmarks/with-ketch/app/layout.tsx
rename to benchmarks/ketch/app/layout.tsx
diff --git a/benchmarks/with-ketch/app/page.tsx b/benchmarks/ketch/app/page.tsx
similarity index 100%
rename from benchmarks/with-ketch/app/page.tsx
rename to benchmarks/ketch/app/page.tsx
diff --git a/benchmarks/with-ketch/config.json b/benchmarks/ketch/config.json
similarity index 97%
rename from benchmarks/with-ketch/config.json
rename to benchmarks/ketch/config.json
index a34a64a..6899459 100644
--- a/benchmarks/with-ketch/config.json
+++ b/benchmarks/ketch/config.json
@@ -1,6 +1,6 @@
{
"$schema": "./node_modules/@cookiebench/benchmark-schema/schema.json",
- "name": "with-ketch",
+ "name": "ketch",
"iterations": 20,
"cookieBanner": {
"selectors": ["#ketch-modal"],
diff --git a/benchmarks/ketch/next-env.d.ts b/benchmarks/ketch/next-env.d.ts
new file mode 100644
index 0000000..9edff1c
--- /dev/null
+++ b/benchmarks/ketch/next-env.d.ts
@@ -0,0 +1,6 @@
+///
+///
+import "./.next/types/routes.d.ts";
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/benchmarks/with-onetrust/next.config.ts b/benchmarks/ketch/next.config.ts
similarity index 100%
rename from benchmarks/with-onetrust/next.config.ts
rename to benchmarks/ketch/next.config.ts
diff --git a/benchmarks/with-ketch/package.json b/benchmarks/ketch/package.json
similarity index 54%
rename from benchmarks/with-ketch/package.json
rename to benchmarks/ketch/package.json
index eaf2e4f..0981b59 100644
--- a/benchmarks/with-ketch/package.json
+++ b/benchmarks/ketch/package.json
@@ -1,8 +1,7 @@
{
- "name": "with-ketch",
+ "name": "ketch",
"private": true,
"scripts": {
- "benchmark": "pnpm exec benchmark-cli benchmark",
"build": "next build",
"dev": "next dev",
"fmt": "biome format . --write",
@@ -10,16 +9,19 @@
"start": "next start"
},
"dependencies": {
- "next": "16.0.7",
- "react": "^19.2.1",
- "react-dom": "^19.2.1"
+ "next": "catalog:",
+ "react": "catalog:",
+ "react-dom": "catalog:"
},
"devDependencies": {
"@cookiebench/benchmark-schema": "workspace:*",
"@cookiebench/ts-config": "workspace:*",
- "@types/node": "^24.10.1",
- "@types/react": "^19.2.7",
- "@types/react-dom": "^19.2.3",
- "typescript": "^5.9.3"
+ "@types/node": "catalog:",
+ "@types/react": "catalog:",
+ "@types/react-dom": "catalog:",
+ "typescript": "catalog:"
+ },
+ "engines": {
+ "node": ">=20.9.0"
}
}
diff --git a/benchmarks/with-osano/tsconfig.json b/benchmarks/ketch/tsconfig.json
similarity index 100%
rename from benchmarks/with-osano/tsconfig.json
rename to benchmarks/ketch/tsconfig.json
diff --git a/benchmarks/with-onetrust/app/favicon.ico b/benchmarks/onetrust/app/favicon.ico
similarity index 100%
rename from benchmarks/with-onetrust/app/favicon.ico
rename to benchmarks/onetrust/app/favicon.ico
diff --git a/benchmarks/with-onetrust/app/layout.tsx b/benchmarks/onetrust/app/layout.tsx
similarity index 81%
rename from benchmarks/with-onetrust/app/layout.tsx
rename to benchmarks/onetrust/app/layout.tsx
index 9361364..9ea3d73 100644
--- a/benchmarks/with-onetrust/app/layout.tsx
+++ b/benchmarks/onetrust/app/layout.tsx
@@ -1,5 +1,5 @@
import type { Metadata } from "next";
-import { ReactNode } from "react";
+import type { ReactNode } from "react";
export const metadata: Metadata = {
title: "benchmark",
@@ -14,12 +14,13 @@ export default function RootLayout({
diff --git a/benchmarks/with-onetrust/app/page.tsx b/benchmarks/onetrust/app/page.tsx
similarity index 100%
rename from benchmarks/with-onetrust/app/page.tsx
rename to benchmarks/onetrust/app/page.tsx
diff --git a/benchmarks/with-onetrust/config.json b/benchmarks/onetrust/config.json
similarity index 97%
rename from benchmarks/with-onetrust/config.json
rename to benchmarks/onetrust/config.json
index 86db5fd..f65b3d0 100644
--- a/benchmarks/with-onetrust/config.json
+++ b/benchmarks/onetrust/config.json
@@ -1,6 +1,6 @@
{
"$schema": "./node_modules/@cookiebench/benchmark-schema/schema.json",
- "name": "with-onetrust",
+ "name": "onetrust",
"id": "onetrust-banner-sdk",
"iterations": 20,
"cookieBanner": {
diff --git a/benchmarks/onetrust/next-env.d.ts b/benchmarks/onetrust/next-env.d.ts
new file mode 100644
index 0000000..9edff1c
--- /dev/null
+++ b/benchmarks/onetrust/next-env.d.ts
@@ -0,0 +1,6 @@
+///
+///
+import "./.next/types/routes.d.ts";
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/benchmarks/with-osano/next.config.ts b/benchmarks/onetrust/next.config.ts
similarity index 100%
rename from benchmarks/with-osano/next.config.ts
rename to benchmarks/onetrust/next.config.ts
diff --git a/benchmarks/with-osano/package.json b/benchmarks/onetrust/package.json
similarity index 52%
rename from benchmarks/with-osano/package.json
rename to benchmarks/onetrust/package.json
index 4189226..745b597 100644
--- a/benchmarks/with-osano/package.json
+++ b/benchmarks/onetrust/package.json
@@ -1,8 +1,7 @@
{
- "name": "with-osano",
+ "name": "onetrust",
"private": true,
"scripts": {
- "benchmark": "pnpm exec benchmark-cli benchmark",
"build": "next build",
"dev": "next dev --port 3006",
"fmt": "biome format . --write",
@@ -10,17 +9,19 @@
"start": "next start"
},
"dependencies": {
- "next": "16.0.7",
- "react": "^19.2.1",
- "react-dom": "^19.2.1"
+ "next": "catalog:",
+ "react": "catalog:",
+ "react-dom": "catalog:"
},
"devDependencies": {
"@cookiebench/benchmark-schema": "workspace:*",
- "@cookiebench/cli": "workspace:*",
"@cookiebench/ts-config": "workspace:*",
- "@types/node": "^24.10.1",
- "@types/react": "^19.2.7",
- "@types/react-dom": "^19.2.3",
- "typescript": "^5.9.3"
+ "@types/node": "catalog:",
+ "@types/react": "catalog:",
+ "@types/react-dom": "catalog:",
+ "typescript": "catalog:"
+ },
+ "engines": {
+ "node": ">=20.9.0"
}
}
diff --git a/benchmarks/with-usercentrics/tsconfig.json b/benchmarks/onetrust/tsconfig.json
similarity index 100%
rename from benchmarks/with-usercentrics/tsconfig.json
rename to benchmarks/onetrust/tsconfig.json
diff --git a/benchmarks/with-osano/app/favicon.ico b/benchmarks/osano/app/favicon.ico
similarity index 100%
rename from benchmarks/with-osano/app/favicon.ico
rename to benchmarks/osano/app/favicon.ico
diff --git a/benchmarks/osano/app/layout.tsx b/benchmarks/osano/app/layout.tsx
new file mode 100644
index 0000000..1098080
--- /dev/null
+++ b/benchmarks/osano/app/layout.tsx
@@ -0,0 +1,25 @@
+import type { Metadata } from "next";
+import Script from "next/script";
+import type { ReactNode } from "react";
+
+export const metadata: Metadata = {
+ title: "benchmark",
+};
+
+export default function RootLayout({
+ children,
+}: Readonly<{
+ children: ReactNode;
+}>) {
+ return (
+
+
+
+ {children}
+
+
+ );
+}
diff --git a/benchmarks/with-osano/app/page.tsx b/benchmarks/osano/app/page.tsx
similarity index 100%
rename from benchmarks/with-osano/app/page.tsx
rename to benchmarks/osano/app/page.tsx
diff --git a/benchmarks/with-osano/config.json b/benchmarks/osano/config.json
similarity index 94%
rename from benchmarks/with-osano/config.json
rename to benchmarks/osano/config.json
index fb25654..7d90dc1 100644
--- a/benchmarks/with-osano/config.json
+++ b/benchmarks/osano/config.json
@@ -1,6 +1,6 @@
{
"$schema": "./node_modules/@cookiebench/benchmark-schema/schema.json",
- "name": "with-osano",
+ "name": "osano",
"id": "osano-banner-sdk",
"iterations": 20,
"cookieBanner": {
@@ -30,7 +30,7 @@
},
"company": {
"name": "Osano",
- "website": "https://Osano.com",
+ "website": "https://www.osano.com",
"avatar": "https://pbs.twimg.com/profile_images/1678452109229043712/Y6oQn0Sq_400x400.jpg"
},
"includes": {
diff --git a/benchmarks/osano/next-env.d.ts b/benchmarks/osano/next-env.d.ts
new file mode 100644
index 0000000..9edff1c
--- /dev/null
+++ b/benchmarks/osano/next-env.d.ts
@@ -0,0 +1,6 @@
+///
+///
+import "./.next/types/routes.d.ts";
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/benchmarks/with-usercentrics/next.config.ts b/benchmarks/osano/next.config.ts
similarity index 100%
rename from benchmarks/with-usercentrics/next.config.ts
rename to benchmarks/osano/next.config.ts
diff --git a/benchmarks/osano/package.json b/benchmarks/osano/package.json
new file mode 100644
index 0000000..1a9b762
--- /dev/null
+++ b/benchmarks/osano/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "osano",
+ "private": true,
+ "scripts": {
+ "build": "next build",
+ "dev": "next dev --port 3007",
+ "fmt": "biome format . --write",
+ "lint": "biome lint .",
+ "start": "next start"
+ },
+ "dependencies": {
+ "next": "catalog:",
+ "react": "catalog:",
+ "react-dom": "catalog:"
+ },
+ "devDependencies": {
+ "@cookiebench/benchmark-schema": "workspace:*",
+ "@cookiebench/ts-config": "workspace:*",
+ "@types/node": "catalog:",
+ "@types/react": "catalog:",
+ "@types/react-dom": "catalog:",
+ "typescript": "catalog:"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ }
+}
diff --git a/benchmarks/osano/tsconfig.json b/benchmarks/osano/tsconfig.json
new file mode 100644
index 0000000..787e3f6
--- /dev/null
+++ b/benchmarks/osano/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "extends": "@cookiebench/ts-config/nextjs.json",
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./*"]
+ }
+ },
+ "include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"],
+ "exclude": ["node_modules"]
+}
diff --git a/benchmarks/with-usercentrics/app/favicon.ico b/benchmarks/usercentrics/app/favicon.ico
similarity index 100%
rename from benchmarks/with-usercentrics/app/favicon.ico
rename to benchmarks/usercentrics/app/favicon.ico
diff --git a/benchmarks/with-usercentrics/app/layout.tsx b/benchmarks/usercentrics/app/layout.tsx
similarity index 100%
rename from benchmarks/with-usercentrics/app/layout.tsx
rename to benchmarks/usercentrics/app/layout.tsx
diff --git a/benchmarks/with-usercentrics/app/page.tsx b/benchmarks/usercentrics/app/page.tsx
similarity index 100%
rename from benchmarks/with-usercentrics/app/page.tsx
rename to benchmarks/usercentrics/app/page.tsx
diff --git a/benchmarks/with-usercentrics/config.json b/benchmarks/usercentrics/config.json
similarity index 97%
rename from benchmarks/with-usercentrics/config.json
rename to benchmarks/usercentrics/config.json
index d899486..0ad0df0 100644
--- a/benchmarks/with-usercentrics/config.json
+++ b/benchmarks/usercentrics/config.json
@@ -1,6 +1,6 @@
{
"$schema": "./node_modules/@cookiebench/benchmark-schema/schema.json",
- "name": "with-usercentrics",
+ "name": "usercentrics",
"id": "usercentrics-banner",
"iterations": 20,
"cookieBanner": {
diff --git a/benchmarks/usercentrics/next-env.d.ts b/benchmarks/usercentrics/next-env.d.ts
new file mode 100644
index 0000000..9edff1c
--- /dev/null
+++ b/benchmarks/usercentrics/next-env.d.ts
@@ -0,0 +1,6 @@
+///
+///
+import "./.next/types/routes.d.ts";
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/benchmarks/with-enzuzo/next.config.ts b/benchmarks/usercentrics/next.config.ts
similarity index 69%
rename from benchmarks/with-enzuzo/next.config.ts
rename to benchmarks/usercentrics/next.config.ts
index a67a28b..7921f35 100644
--- a/benchmarks/with-enzuzo/next.config.ts
+++ b/benchmarks/usercentrics/next.config.ts
@@ -1,4 +1,4 @@
-import type { NextConfig } from 'next';
+import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
diff --git a/benchmarks/with-usercentrics/package.json b/benchmarks/usercentrics/package.json
similarity index 51%
rename from benchmarks/with-usercentrics/package.json
rename to benchmarks/usercentrics/package.json
index eb22797..bfee769 100644
--- a/benchmarks/with-usercentrics/package.json
+++ b/benchmarks/usercentrics/package.json
@@ -1,8 +1,7 @@
{
- "name": "with-usercentrics",
+ "name": "usercentrics",
"private": true,
"scripts": {
- "benchmark": "pnpm exec benchmark-cli benchmark",
"build": "next build",
"dev": "next dev --port 3001",
"fmt": "biome format . --write",
@@ -10,17 +9,19 @@
"start": "next start"
},
"dependencies": {
- "next": "16.0.7",
- "react": "^19.2.1",
- "react-dom": "^19.2.1"
+ "next": "catalog:",
+ "react": "catalog:",
+ "react-dom": "catalog:"
},
"devDependencies": {
"@cookiebench/benchmark-schema": "workspace:*",
- "@cookiebench/cli": "workspace:*",
"@cookiebench/ts-config": "workspace:*",
- "@types/node": "^24.10.1",
- "@types/react": "^19.2.7",
- "@types/react-dom": "^19.2.3",
- "typescript": "^5.9.3"
+ "@types/node": "catalog:",
+ "@types/react": "catalog:",
+ "@types/react-dom": "catalog:",
+ "typescript": "catalog:"
+ },
+ "engines": {
+ "node": ">=20.9.0"
}
}
diff --git a/benchmarks/usercentrics/tsconfig.json b/benchmarks/usercentrics/tsconfig.json
new file mode 100644
index 0000000..787e3f6
--- /dev/null
+++ b/benchmarks/usercentrics/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "extends": "@cookiebench/ts-config/nextjs.json",
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./*"]
+ }
+ },
+ "include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"],
+ "exclude": ["node_modules"]
+}
diff --git a/benchmarks/with-c15t-nextjs/package.json b/benchmarks/with-c15t-nextjs/package.json
deleted file mode 100644
index 075fd2c..0000000
--- a/benchmarks/with-c15t-nextjs/package.json
+++ /dev/null
@@ -1,28 +0,0 @@
-{
- "name": "with-c15t-nextjs",
- "version": "0.1.0",
- "private": true,
- "scripts": {
- "benchmark": "pnpm exec benchmark-cli benchmark",
- "build": "next build",
- "dev": "next dev --port 3001",
- "fmt": "biome format . --write",
- "lint": "biome lint .",
- "start": "next start"
- },
- "dependencies": {
- "@c15t/nextjs": "1.8.1",
- "next": "16.0.7",
- "react": "^19.2.1",
- "react-dom": "^19.2.1"
- },
- "devDependencies": {
- "@cookiebench/benchmark-schema": "workspace:*",
- "@cookiebench/cli": "workspace:*",
- "@cookiebench/ts-config": "workspace:*",
- "@types/node": "^24.10.1",
- "@types/react": "^19.2.7",
- "@types/react-dom": "^19.2.3",
- "typescript": "^5.9.3"
- }
-}
diff --git a/benchmarks/with-c15t-react/package.json b/benchmarks/with-c15t-react/package.json
deleted file mode 100644
index a66b2ef..0000000
--- a/benchmarks/with-c15t-react/package.json
+++ /dev/null
@@ -1,28 +0,0 @@
-{
- "name": "with-c15t-react",
- "private": true,
- "scripts": {
- "benchmark": "pnpm exec benchmark-cli benchmark",
- "build": "next build",
- "dev": "next dev --port 3003",
- "fmt": "biome format . --write",
- "lint": "biome lint .",
- "start": "next start --port 3003"
- },
- "dependencies": {
- "@c15t/react": "1.8.1",
- "@c15t/translations": "^1.8.0",
- "next": "16.0.7",
- "react": "^19.2.1",
- "react-dom": "^19.2.1"
- },
- "devDependencies": {
- "@cookiebench/benchmark-schema": "workspace:*",
- "@cookiebench/cli": "workspace:*",
- "@cookiebench/ts-config": "workspace:*",
- "@types/node": "^24.10.1",
- "@types/react": "^19.2.7",
- "@types/react-dom": "^19.2.3",
- "typescript": "^5.9.3"
- }
-}
diff --git a/benchmarks/with-cookie-control/config.json b/benchmarks/with-cookie-control/config.json
deleted file mode 100644
index 502dd42..0000000
--- a/benchmarks/with-cookie-control/config.json
+++ /dev/null
@@ -1,40 +0,0 @@
-{
- "$schema": "./node_modules/@cookiebench/benchmark-schema/schema.json",
- "name": "with-cookie-control",
- "id": "cookie-control",
- "iterations": 20,
- "cookieBanner": {
- "selectors": [".ccc-module--slideout"],
- "serviceHosts": ["civiccomputing.com"],
- "waitForVisibility": true,
- "measureViewportCoverage": true,
- "expectedLayoutShift": true,
- "serviceName": "Cookie Control"
- },
- "internationalization": {
- "detection": "none",
- "stringLoading": "none"
- },
- "techStack": {
- "bundler": "unknown",
- "bundleType": "iffe",
- "frameworks": [],
- "languages": ["javascript"],
- "packageManager": "unknown",
- "typescript": false
- },
- "source": {
- "isOpenSource": false,
- "license": "proprietary",
- "website": "https://www.civicuk.com/cookie-control/"
- },
- "company": {
- "name": "Cookie Control",
- "website": "https://www.civicuk.com/cookie-control/",
- "avatar": "https://www.civicuk.com/cookie-control/images/favicon.png"
- },
- "includes": {
- "backend": ["proprietary"],
- "components": ["javascript"]
- }
-}
diff --git a/benchmarks/with-cookie-control/next.config.ts b/benchmarks/with-cookie-control/next.config.ts
deleted file mode 100644
index a67a28b..0000000
--- a/benchmarks/with-cookie-control/next.config.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import type { NextConfig } from 'next';
-
-const nextConfig: NextConfig = {
- /* config options here */
-};
-
-export default nextConfig;
diff --git a/benchmarks/with-cookie-control/tsconfig.json b/benchmarks/with-cookie-control/tsconfig.json
deleted file mode 100644
index 99f37b5..0000000
--- a/benchmarks/with-cookie-control/tsconfig.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "extends": "@cookiebench/ts-config/nextjs.json",
- "compilerOptions": {
- "baseUrl": ".",
- "paths": {
- "@/*": ["./*"]
- }
- },
- "include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"],
- "exclude": ["node_modules"]
-}
diff --git a/benchmarks/with-didomi/app/layout.tsx b/benchmarks/with-didomi/app/layout.tsx
deleted file mode 100644
index 05047ed..0000000
--- a/benchmarks/with-didomi/app/layout.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-"use client";
-
-import { DidomiSDK, type IDidomiObject } from "@didomi/react";
-import { type ReactNode, useCallback, useState } from "react";
-
-export default function RootLayout({
- children,
-}: Readonly<{
- children: ReactNode;
-}>) {
- const [didomiObject, setDidomiObject] = useState(null);
-
- const onDidomiReady = useCallback((didomi: IDidomiObject) => {
- setDidomiObject(didomi);
- console.log(
- "Didomi Ready - Is consent required?:",
- didomi.isConsentRequired()
- );
- console.log(
- "Didomi Ready - Consent for vendor IAB 1:",
- didomi.getUserConsentStatusForVendor(1)
- );
- console.log(
- "Didomi Ready - Consent for vendor IAB 1 and cookies:",
- didomi.getUserConsentStatus("cookies", 1)
- );
- }, []);
-
- const onConsentChanged = useCallback(
- (cwtToken: string) => {
- if (!didomiObject) return;
- console.log("Didomi Consent Changed - cwtToken:", cwtToken);
- console.log(
- "Didomi Consent Changed - Is consent required?:",
- didomiObject.isConsentRequired()
- );
- console.log(
- "Didomi Consent Changed - Consent for vendor IAB 1:",
- didomiObject.getUserConsentStatusForVendor(1)
- );
- console.log(
- "Didomi Consent Changed - Consent for vendor IAB 1 and cookies:",
- didomiObject.getUserConsentStatus("cookies", 1)
- );
- },
- [didomiObject]
- );
-
- return (
-
-
- console.log("Didomi Notice Shown")}
- onNoticeHidden={() => console.log("Didomi Notice Hidden")}
- />
- {children}
-
-
- );
-}
diff --git a/benchmarks/with-didomi/next-env.d.ts b/benchmarks/with-didomi/next-env.d.ts
deleted file mode 100644
index 1b3be08..0000000
--- a/benchmarks/with-didomi/next-env.d.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-///
-///
-
-// NOTE: This file should not be edited
-// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/benchmarks/with-enzuzo/app/layout.tsx b/benchmarks/with-enzuzo/app/layout.tsx
deleted file mode 100644
index 84fc0f6..0000000
--- a/benchmarks/with-enzuzo/app/layout.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import type { Metadata } from 'next';
-import type { ReactNode } from 'react';
-
-export const metadata: Metadata = {
- title: 'benchmark',
-};
-
-export default function RootLayout({
- children,
-}: Readonly<{
- children: ReactNode;
-}>) {
- return (
-
-
- {/* Cookie Control Script */}
-
- {/* */}
-
- {children}
-
- );
-}
diff --git a/benchmarks/with-enzuzo/config.json b/benchmarks/with-enzuzo/config.json
deleted file mode 100644
index 3595384..0000000
--- a/benchmarks/with-enzuzo/config.json
+++ /dev/null
@@ -1,40 +0,0 @@
-{
- "$schema": "./node_modules/@cookiebench/benchmark-schema/schema.json",
- "name": "with-enzuzo",
- "id": "enzuzo",
- "iterations": 20,
- "cookieBanner": {
- "selectors": [".enzuzo-cookiebanner-container"],
- "serviceHosts": ["app.enzuzo.com"],
- "waitForVisibility": true,
- "measureViewportCoverage": true,
- "expectedLayoutShift": true,
- "serviceName": "Enzuzo"
- },
- "internationalization": {
- "detection": "none",
- "stringLoading": "none"
- },
- "techStack": {
- "bundler": "unknown",
- "bundleType": "iffe",
- "frameworks": [],
- "languages": ["javascript"],
- "packageManager": "unknown",
- "typescript": false
- },
- "source": {
- "isOpenSource": false,
- "license": "proprietary",
- "website": "https://www.enzuzo.com/"
- },
- "company": {
- "name": "Enzuzo",
- "website": "https://www.enzuzo.com/",
- "avatar": "https://pbs.twimg.com/profile_images/1519664904562786305/h7G0tW79_400x400.jpg"
- },
- "includes": {
- "backend": ["proprietary"],
- "components": ["javascript"]
- }
-}
diff --git a/benchmarks/with-enzuzo/next-env.d.ts b/benchmarks/with-enzuzo/next-env.d.ts
deleted file mode 100644
index 1b3be08..0000000
--- a/benchmarks/with-enzuzo/next-env.d.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-///
-///
-
-// NOTE: This file should not be edited
-// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/benchmarks/with-enzuzo/tsconfig.json b/benchmarks/with-enzuzo/tsconfig.json
deleted file mode 100644
index 99f37b5..0000000
--- a/benchmarks/with-enzuzo/tsconfig.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "extends": "@cookiebench/ts-config/nextjs.json",
- "compilerOptions": {
- "baseUrl": ".",
- "paths": {
- "@/*": ["./*"]
- }
- },
- "include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"],
- "exclude": ["node_modules"]
-}
diff --git a/benchmarks/with-iubenda/next-env.d.ts b/benchmarks/with-iubenda/next-env.d.ts
deleted file mode 100644
index 1b3be08..0000000
--- a/benchmarks/with-iubenda/next-env.d.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-///
-///
-
-// NOTE: This file should not be edited
-// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/benchmarks/with-ketch/next-env.d.ts b/benchmarks/with-ketch/next-env.d.ts
deleted file mode 100644
index 1b3be08..0000000
--- a/benchmarks/with-ketch/next-env.d.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-///
-///
-
-// NOTE: This file should not be edited
-// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/benchmarks/with-onetrust/next-env.d.ts b/benchmarks/with-onetrust/next-env.d.ts
deleted file mode 100644
index 1b3be08..0000000
--- a/benchmarks/with-onetrust/next-env.d.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-///
-///
-
-// NOTE: This file should not be edited
-// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/benchmarks/with-onetrust/package.json b/benchmarks/with-onetrust/package.json
deleted file mode 100644
index d8297b7..0000000
--- a/benchmarks/with-onetrust/package.json
+++ /dev/null
@@ -1,26 +0,0 @@
-{
- "name": "with-onetrust",
- "private": true,
- "scripts": {
- "benchmark": "pnpm exec benchmark-cli benchmark",
- "build": "next build",
- "dev": "next dev --port 3006",
- "fmt": "biome format . --write",
- "lint": "biome lint .",
- "start": "next start"
- },
- "dependencies": {
- "next": "16.0.7",
- "react": "^19.2.1",
- "react-dom": "^19.2.1"
- },
- "devDependencies": {
- "@cookiebench/benchmark-schema": "workspace:*",
- "@cookiebench/cli": "workspace:*",
- "@cookiebench/ts-config": "workspace:*",
- "@types/node": "^24.10.1",
- "@types/react": "^19.2.7",
- "@types/react-dom": "^19.2.3",
- "typescript": "^5.9.3"
- }
-}
diff --git a/benchmarks/with-osano/next-env.d.ts b/benchmarks/with-osano/next-env.d.ts
deleted file mode 100644
index 1b3be08..0000000
--- a/benchmarks/with-osano/next-env.d.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-///
-///
-
-// NOTE: This file should not be edited
-// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/benchmarks/with-usercentrics/next-env.d.ts b/benchmarks/with-usercentrics/next-env.d.ts
deleted file mode 100644
index 1b3be08..0000000
--- a/benchmarks/with-usercentrics/next-env.d.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-///
-///
-
-// NOTE: This file should not be edited
-// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/package.json b/package.json
index a764c97..724edc7 100644
--- a/package.json
+++ b/package.json
@@ -1,31 +1,31 @@
{
- "name": "cookiebench",
- "private": true,
- "scripts": {
- "benchmark": "turbo run benchmark",
- "build": "turbo run build",
- "check-types": "turbo run check-types",
- "db": "pnpm exec benchmark-cli db",
- "dev": "turbo run dev --filter=benchmarks",
- "fmt": "turbo fmt",
- "lint": "turbo run lint",
- "results": "pnpm exec benchmark-cli results"
- },
- "devDependencies": {
- "@biomejs/biome": "2.3.8",
- "@c15t/translations": "^1.8.0",
- "@cookiebench/cli": "workspace:*",
- "@playwright/test": "^1.57.0",
- "cli-table3": "^0.6.5",
- "drizzle-kit": "^0.31.8",
- "p-limit": "^7.2.0",
- "pretty-ms": "^9.3.0",
- "turbo": "^2.6.3",
- "typescript": "5.9.3",
- "ultracite": "^6.3.9"
- },
- "packageManager": "pnpm@9.0.0",
- "engines": {
- "node": ">=18"
- }
-}
\ No newline at end of file
+ "name": "cookiebench",
+ "private": true,
+ "scripts": {
+ "benchmark": "pnpm turbo run build --filter=./packages/cookiebench-cli... && pnpm --filter ./packages/cookiebench-cli run start benchmark",
+ "build": "turbo run build",
+ "check-types": "turbo run check-types",
+ "db": "pnpm turbo run build --filter=./packages/cookiebench-cli... && pnpm --filter ./packages/cookiebench-cli run start db",
+ "dev": "turbo run dev --filter=benchmarks",
+ "fmt": "turbo fmt",
+ "lint": "turbo run lint",
+ "results": "pnpm turbo run build --filter=./packages/cookiebench-cli... && pnpm --filter ./packages/cookiebench-cli run start results"
+ },
+ "devDependencies": {
+ "@biomejs/biome": "2.3.2",
+ "@consentio/benchmark": "workspace:*",
+ "@consentio/runner": "workspace:*",
+ "@playwright/test": "^1.58.2",
+ "cli-table3": "^0.6.5",
+ "cookiebench": "workspace:*",
+ "drizzle-kit": "^0.31.9",
+ "pretty-ms": "^9.3.0",
+ "turbo": "^2.8.9",
+ "typescript": "5.9.3",
+ "ultracite": "6.0.5"
+ },
+ "packageManager": "pnpm@10.29.3",
+ "engines": {
+ "node": ">=18"
+ }
+}
diff --git a/packages/benchmark-schema/schema.json b/packages/benchmark-schema/schema.json
index d3cf071..3b93cd0 100644
--- a/packages/benchmark-schema/schema.json
+++ b/packages/benchmark-schema/schema.json
@@ -3,12 +3,7 @@
"title": "Benchmark Configuration Schema",
"description": "Schema for cookie banner benchmark configurations",
"type": "object",
- "required": [
- "name",
- "iterations",
- "cookieBanner",
- "techStack"
- ],
+ "required": ["name", "iterations", "cookieBanner", "techStack"],
"properties": {
"name": {
"type": "string",
@@ -53,10 +48,7 @@
},
"cookieBanner": {
"type": "object",
- "required": [
- "selectors",
- "serviceHosts"
- ],
+ "required": ["selectors", "serviceHosts"],
"properties": {
"selectors": {
"type": "array",
@@ -95,28 +87,16 @@
},
"internationalization": {
"type": "object",
- "required": [
- "detection",
- "stringLoading"
- ],
+ "required": ["detection", "stringLoading"],
"properties": {
"detection": {
"type": "string",
- "enum": [
- "browser",
- "ip",
- "manual",
- "none"
- ],
+ "enum": ["browser", "ip", "manual", "none"],
"description": "Method used to determine the banner language"
},
"stringLoading": {
"type": "string",
- "enum": [
- "bundled",
- "server",
- "none"
- ],
+ "enum": ["bundled", "server", "none"],
"description": "How the banner loads its translation strings"
}
}
@@ -138,21 +118,13 @@
"oneOf": [
{
"type": "string",
- "enum": [
- "esm",
- "cjs",
- "iffe",
- "bundled"
- ]
+ "enum": ["esm", "cjs", "iife", "bundled"]
},
{
"type": "array",
"items": {
"type": "string",
- "enum": [
- "esm",
- "cjs"
- ]
+ "enum": ["esm", "cjs"]
}
}
]
@@ -167,10 +139,7 @@
"type": "array",
"items": {
"type": "string",
- "enum": [
- "typescript",
- "javascript"
- ]
+ "enum": ["typescript", "javascript"]
}
},
"packageManager": {
@@ -194,9 +163,7 @@
},
{
"type": "string",
- "enum": [
- "partially"
- ]
+ "enum": ["partially"]
}
]
},
@@ -259,4 +226,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/packages/benchmark/README.md b/packages/benchmark/README.md
new file mode 100644
index 0000000..574b95b
--- /dev/null
+++ b/packages/benchmark/README.md
@@ -0,0 +1,113 @@
+# @consentio/benchmark
+
+Core benchmark measurement logic for cookie banner performance testing.
+
+## Overview
+
+This package provides the core functionality for detecting and measuring cookie banner performance impact. It includes collectors for cookie banners, network monitoring, and resource timing.
+
+## Features
+
+- **Cookie Banner Detection**: Automatically detects cookie banners using configurable selectors
+- **Network Monitoring**: Tracks network requests and calculates size/timing metrics
+- **Resource Collection**: Collects detailed resource timing data from the browser
+- **Bundle Strategy Detection**: Identifies bundling approaches (IIFE, ESM, CJS, bundled)
+- **Performance Metrics**: Measures layout shift, render time, and viewport coverage
+
+## Installation
+
+```bash
+pnpm add @consentio/benchmark
+```
+
+## Usage
+
+```typescript
+import {
+ CookieBannerCollector,
+ NetworkMonitor,
+ ResourceTimingCollector,
+ determineBundleStrategy,
+ BENCHMARK_CONSTANTS,
+} from '@consentio/benchmark';
+import { chromium } from '@playwright/test';
+
+// Create config
+const config = {
+ name: 'my-app',
+ iterations: 5,
+ cookieBanner: {
+ selectors: ['.cookie-banner', '#cookie-consent'],
+ serviceHosts: ['cookiecdn.com'],
+ serviceName: 'CookieService',
+ waitForVisibility: true,
+ measureViewportCoverage: true,
+ expectedLayoutShift: true,
+ },
+ techStack: {
+ bundleType: 'esm',
+ // ...
+ },
+ // ...
+};
+
+// Initialize collectors
+const cookieBannerCollector = new CookieBannerCollector(config);
+const networkMonitor = new NetworkMonitor(config);
+const resourceCollector = new ResourceTimingCollector();
+
+// Use with Playwright
+const browser = await chromium.launch();
+const page = await browser.newPage();
+
+// Setup detection and monitoring
+await cookieBannerCollector.setupDetection(page);
+await networkMonitor.setupMonitoring(page);
+
+// Navigate to page
+await page.goto('https://example.com');
+
+// Collect metrics
+const bannerData = await cookieBannerCollector.collectMetrics(page);
+const resourceData = await resourceCollector.collect(page);
+const networkRequests = networkMonitor.getNetworkRequests();
+
+await browser.close();
+```
+
+## API
+
+### CookieBannerCollector
+
+- `constructor(config: Config)`: Create a new collector
+- `initializeMetrics()`: Initialize cookie banner metrics tracking
+- `setupDetection(page: Page)`: Set up browser-side detection script
+- `collectMetrics(page: Page)`: Collect metrics from the page
+
+### NetworkMonitor
+
+- `constructor(config: Config)`: Create a new monitor
+- `setupMonitoring(page: Page)`: Set up network request interception
+- `getNetworkRequests()`: Get collected network requests
+- `getMetrics()`: Get network metrics
+- `calculateNetworkImpact()`: Calculate network impact metrics
+- `reset()`: Reset collected data
+
+### ResourceTimingCollector
+
+- `collect(page: Page)`: Collect detailed resource timing data
+
+### Utilities
+
+- `determineBundleStrategy(config: Config)`: Determine bundle strategy from config
+- `BENCHMARK_CONSTANTS`: Constants for detection intervals, timeouts, etc.
+- `BUNDLE_TYPES`: Bundle type constants (IIFE, ESM, CJS, BUNDLED)
+
+## Types
+
+See the [types file](./src/types.ts) for complete type definitions.
+
+## License
+
+MIT
+
diff --git a/packages/benchmark/package.json b/packages/benchmark/package.json
new file mode 100644
index 0000000..b53fe49
--- /dev/null
+++ b/packages/benchmark/package.json
@@ -0,0 +1,37 @@
+{
+ "name": "@consentio/benchmark",
+ "version": "0.0.1",
+ "type": "module",
+ "exports": {
+ ".": {
+ "import": "./dist/index.js",
+ "types": "./dist/index.d.ts"
+ }
+ },
+ "main": "./dist/index.js",
+ "module": "./dist/index.js",
+ "types": "./dist/index.d.ts",
+ "scripts": {
+ "build": "rslib build",
+ "check-types": "tsc --noEmit",
+ "dev": "rslib build --watch",
+ "fmt": "biome format . --write",
+ "lint": "biome lint ."
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "dependencies": {
+ "@c15t/logger": "^1.0.1",
+ "@consentio/shared": "workspace:*",
+ "@playwright/test": "^1.58.2",
+ "perfume.js": "^9.4.0",
+ "playwright-performance-metrics": "^1.2.5"
+ },
+ "devDependencies": {
+ "@rsdoctor/rspack-plugin": "^1.5.2",
+ "@rslib/core": "^0.16.1",
+ "@types/node": "catalog:",
+ "typescript": "catalog:"
+ }
+}
diff --git a/packages/benchmark/rslib.config.ts b/packages/benchmark/rslib.config.ts
new file mode 100644
index 0000000..434432c
--- /dev/null
+++ b/packages/benchmark/rslib.config.ts
@@ -0,0 +1,14 @@
+import { defineConfig } from "@rslib/core";
+
+export default defineConfig({
+ lib: [
+ {
+ format: "esm",
+ syntax: "es2021",
+ dts: true,
+ },
+ ],
+ output: {
+ target: "node",
+ },
+});
diff --git a/packages/benchmark/src/bundle-strategy.ts b/packages/benchmark/src/bundle-strategy.ts
new file mode 100644
index 0000000..0030cc1
--- /dev/null
+++ b/packages/benchmark/src/bundle-strategy.ts
@@ -0,0 +1,35 @@
+import { BUNDLE_TYPES } from "./constants";
+import type { BundleStrategy, Config } from "./types";
+
+export function determineBundleStrategy(config: Config): BundleStrategy {
+ const bundleType = config.techStack?.bundleType;
+ const rawBundleType = bundleType as string | string[] | undefined;
+ const legacyIifeType = "iife";
+
+ const isIIFE =
+ rawBundleType === BUNDLE_TYPES.IIFE ||
+ rawBundleType === BUNDLE_TYPES.IFFE ||
+ rawBundleType === legacyIifeType ||
+ (Array.isArray(rawBundleType) &&
+ (rawBundleType.includes(BUNDLE_TYPES.IIFE) ||
+ rawBundleType.includes(BUNDLE_TYPES.IFFE) ||
+ rawBundleType.includes(legacyIifeType)));
+
+ const isModuleBundleType =
+ rawBundleType === BUNDLE_TYPES.ESM ||
+ rawBundleType === BUNDLE_TYPES.CJS ||
+ rawBundleType === BUNDLE_TYPES.BUNDLED;
+
+ const isArrayWithModules =
+ Array.isArray(rawBundleType) &&
+ (rawBundleType.includes(BUNDLE_TYPES.ESM) ||
+ rawBundleType.includes(BUNDLE_TYPES.CJS) ||
+ rawBundleType.includes(BUNDLE_TYPES.BUNDLED) ||
+ rawBundleType.includes(BUNDLE_TYPES.IIFE) ||
+ rawBundleType.includes(BUNDLE_TYPES.IFFE) ||
+ rawBundleType.includes(legacyIifeType));
+
+ const isBundled = !isIIFE && (isModuleBundleType || isArrayWithModules);
+
+ return { isBundled, isIIFE, bundleType };
+}
diff --git a/packages/benchmark/src/constants.ts b/packages/benchmark/src/constants.ts
new file mode 100644
index 0000000..cdb8456
--- /dev/null
+++ b/packages/benchmark/src/constants.ts
@@ -0,0 +1,82 @@
+import {
+ BYTES_TO_KB,
+ ONE_SECOND,
+ PERCENTAGE_MULTIPLIER,
+ TTI_BUFFER_MS,
+} from "@consentio/shared";
+
+/**
+ * Benchmark constants used throughout the measurement system.
+ *
+ * These constants define timing windows, thresholds, and intervals for
+ * cookie banner detection and metrics collection. All values are measured
+ * in milliseconds unless otherwise specified.
+ *
+ * @see METHODOLOGY.md for detailed explanation of measurement approach
+ */
+export const BENCHMARK_CONSTANTS = {
+ /**
+ * Wait 1 second between detection attempts when banner is not found immediately.
+ * Prevents excessive polling while ensuring we catch dynamically loaded banners.
+ */
+ DETECTION_INTERVAL: ONE_SECOND, // Wait 1 second between detection attempts
+ /**
+ * Maximum time to wait for banner detection before giving up.
+ * Increased to 15 seconds to accommodate longer waits for async-loaded banners.
+ */
+ MAX_DETECTION_TIME: 15_000, // Increased to 15 seconds to accommodate longer waits
+ /**
+ * Initial delay before starting banner detection.
+ * Allows page to start loading before we begin checking for banner.
+ */
+ INITIAL_DETECTION_DELAY: 500, // Wait 500ms before starting
+ /**
+ * Buffer time added to Time to Interactive calculations.
+ * Ensures page is truly interactive before recording TTI.
+ */
+ TTI_BUFFER: TTI_BUFFER_MS,
+ /**
+ * Timeout for collecting performance metrics from Perfume.js.
+ * Some metrics may take time to be reported by the browser.
+ */
+ METRICS_TIMEOUT: 10_000,
+ /**
+ * Retry timeout for metrics collection failures.
+ * Allows retry attempts if initial collection fails.
+ */
+ METRICS_RETRY_TIMEOUT: 5000,
+ /**
+ * Conversion factor: bytes to kilobytes.
+ * Used for displaying resource sizes in KB.
+ */
+ BYTES_TO_KB, // Convert bytes to kilobytes
+ /**
+ * Wait time for Perfume.js metrics to be collected.
+ * Perfume.js reports metrics asynchronously, so we wait before collection.
+ */
+ PERFUME_METRICS_WAIT: ONE_SECOND, // Wait 1 second for Perfume.js metrics to be collected
+ /**
+ * Polling interval for banner visibility detection.
+ * Checks every 100ms for banner appearance/visibility changes.
+ * Lower values = more accurate but more CPU usage.
+ */
+ BANNER_POLL_INTERVAL: 100, // Poll for banner visibility every 100ms
+ /**
+ * Timeout for banner detection polling.
+ * Stops checking for banner after 10 seconds to prevent infinite loops.
+ */
+ BANNER_DETECTION_TIMEOUT: 10_000, // Stop checking for banner after 10 seconds
+ /**
+ * Multiplier for converting decimal ratios to percentages.
+ * Used for viewport coverage calculations (0.125 -> 12.5%).
+ */
+ PERCENTAGE_MULTIPLIER, // Convert decimal to percentage
+} as const;
+
+export const BUNDLE_TYPES = {
+ IIFE: "iffe",
+ IFFE: "iffe",
+ ESM: "esm",
+ CJS: "cjs",
+ BUNDLED: "bundled",
+} as const;
diff --git a/packages/benchmark/src/cookie-banner-collector.ts b/packages/benchmark/src/cookie-banner-collector.ts
new file mode 100644
index 0000000..6749eed
--- /dev/null
+++ b/packages/benchmark/src/cookie-banner-collector.ts
@@ -0,0 +1,381 @@
+import type { Logger } from "@c15t/logger";
+import type { Page } from "@playwright/test";
+import { determineBundleStrategy } from "./bundle-strategy";
+import { BENCHMARK_CONSTANTS } from "./constants";
+import type {
+ Config,
+ CookieBannerData,
+ CookieBannerMetrics,
+ LayoutShiftEntry,
+ WindowWithCookieMetrics,
+} from "./types";
+
+/**
+ * Opacity threshold for determining when banner is actually visible to users.
+ *
+ * This threshold (0.5 = 50% opacity) accounts for CSS transitions and animations.
+ * A banner that renders instantly but fades in slowly will only be considered
+ * "visible" once it reaches 50% opacity, ensuring scores reflect actual user
+ * experience rather than just technical render time.
+ *
+ * @see METHODOLOGY.md for detailed explanation of render time vs visibility time
+ */
+const OPACITY_VISIBILITY_THRESHOLD = 0.5;
+
+export class CookieBannerCollector {
+ private readonly config: Config;
+ private readonly logger: Logger;
+
+ constructor(config: Config, logger: Logger) {
+ this.config = config;
+ this.logger = logger;
+ }
+
+ /**
+ * Initialize cookie banner metrics tracking
+ */
+ initializeMetrics(): CookieBannerMetrics {
+ const { isBundled, isIIFE } = determineBundleStrategy(this.config);
+
+ let bundleStrategy = "Unknown";
+ if (isBundled) {
+ bundleStrategy = "Bundled";
+ } else if (isIIFE) {
+ bundleStrategy = "IIFE";
+ }
+
+ this.logger.debug(
+ `Bundle strategy detected from config: ${bundleStrategy}`,
+ {
+ bundleType: this.config.techStack?.bundleType,
+ isBundled,
+ isIIFE,
+ }
+ );
+
+ return {
+ detectionStartTime: 0,
+ bannerRenderTime: 0,
+ bannerInteractiveTime: 0,
+ bannerScriptLoadTime: 0,
+ bannerLayoutShiftImpact: 0,
+ bannerNetworkRequests: 0,
+ bannerBundleSize: 0,
+ bannerMainThreadBlockingTime: 0,
+ isBundled,
+ isIIFE,
+ bannerDetected: false,
+ bannerSelector: null,
+ };
+ }
+
+ /**
+ * Set up cookie banner detection script in the browser.
+ *
+ * This method injects a detection script that runs before page load to track
+ * banner appearance metrics. It measures both technical render time (when element
+ * appears in DOM) and user-perceived visibility time (when opacity > threshold).
+ *
+ * Metrics tracked:
+ * - bannerFirstSeen: Technical render time (element in DOM)
+ * - bannerVisibleTime: User-perceived visibility (opacity > 0.5)
+ * - bannerInteractive: When buttons become clickable
+ *
+ * @see METHODOLOGY.md for detailed measurement approach
+ */
+ async setupDetection(page: Page): Promise {
+ const selectors = this.config.cookieBanner?.selectors || [];
+
+ await page.addInitScript(
+ (config: {
+ bannerSelectors: string[];
+ pollInterval: number;
+ detectionTimeout: number;
+ opacityThreshold: number;
+ }) => {
+ const {
+ bannerSelectors,
+ pollInterval,
+ detectionTimeout,
+ opacityThreshold,
+ } = config;
+ // Store initial performance baseline
+ // pageLoadStart = 0 means all times are relative to navigation start
+ // performance.now() already returns time since navigation (timeOrigin)
+ (window as unknown as WindowWithCookieMetrics).__cookieBannerMetrics = {
+ pageLoadStart: 0,
+ bannerDetectionStart: 0,
+ bannerFirstSeen: 0,
+ bannerVisibleTime: 0, // Track when banner is actually visible (opacity > 0.5)
+ bannerInteractive: 0,
+ layoutShiftsBefore: 0,
+ layoutShiftsAfter: 0,
+ detected: false,
+ selector: null,
+ };
+
+ // Monitor for layout shifts specifically
+ let cumulativeLayoutShift = 0;
+ if ("PerformanceObserver" in window) {
+ // Type guard to safely check if entry is a LayoutShiftEntry
+ const isLayoutShiftEntry = (
+ entry: PerformanceEntry
+ ): entry is LayoutShiftEntry =>
+ entry.entryType === "layout-shift" &&
+ "value" in entry &&
+ typeof entry.value === "number" &&
+ "hadRecentInput" in entry &&
+ typeof entry.hadRecentInput === "boolean";
+
+ const clsObserver = new PerformanceObserver((list) => {
+ for (const entry of list.getEntries()) {
+ if (isLayoutShiftEntry(entry) && !entry.hadRecentInput) {
+ cumulativeLayoutShift += entry.value;
+ (
+ window as unknown as WindowWithCookieMetrics
+ ).__cookieBannerMetrics.layoutShiftsAfter =
+ cumulativeLayoutShift;
+ }
+ }
+ });
+ clsObserver.observe({ type: "layout-shift", buffered: true });
+ // Store observer reference for cleanup
+ (
+ window as unknown as WindowWithCookieMetrics
+ ).__cookieBannerMetrics.clsObserver = clsObserver;
+ }
+
+ // Cookie banner detection logic
+ const detectCookieBanner = () => {
+ const metrics = (window as unknown as WindowWithCookieMetrics)
+ .__cookieBannerMetrics;
+ // performance.now() returns time since navigation start (timeOrigin)
+ metrics.bannerDetectionStart = performance.now();
+
+ for (const selector of bannerSelectors) {
+ try {
+ const element = document.querySelector(selector);
+ if (element) {
+ const rect = element.getBoundingClientRect();
+ const computedStyle = window.getComputedStyle(element);
+ const opacity = Number.parseFloat(computedStyle.opacity);
+
+ /**
+ * Technical render check: Element exists in DOM with dimensions.
+ * This determines when the banner element first appears, regardless
+ * of visibility. Used for bannerRenderTime metric.
+ */
+ const isRendered =
+ rect.width > 0 &&
+ rect.height > 0 &&
+ computedStyle.visibility !== "hidden" &&
+ computedStyle.display !== "none";
+
+ /**
+ * User-perceived visibility check: Element is rendered AND visible.
+ * Uses opacity threshold to account for CSS animations. A banner that
+ * renders instantly but fades in slowly will only be considered "visible"
+ * once opacity > 0.5. This ensures scores reflect actual user experience.
+ * Used for bannerVisibilityTime metric (primary scoring metric).
+ */
+ const isVisible = isRendered && opacity > opacityThreshold;
+
+ if (isRendered) {
+ const bannerMetrics = (
+ window as unknown as WindowWithCookieMetrics
+ ).__cookieBannerMetrics;
+ // performance.now() returns time since navigation start (timeOrigin)
+ const now = performance.now();
+
+ /**
+ * Track technical render time: When banner element first appears in DOM.
+ * This is measured from navigationStart and represents when the element
+ * has dimensions and is not hidden. Used for bannerRenderTime metric.
+ */
+ if (!bannerMetrics.detected) {
+ bannerMetrics.detected = true;
+ bannerMetrics.selector = selector;
+ bannerMetrics.bannerFirstSeen = now;
+ bannerMetrics.layoutShiftsBefore = cumulativeLayoutShift;
+ }
+
+ /**
+ * Track user-perceived visibility time: When banner becomes visible to users.
+ * Only recorded when opacity > threshold (0.5), accounting for CSS animations.
+ * This is the primary metric used for scoring (bannerVisibilityTime).
+ * Only update if not set yet or if this measurement is earlier.
+ */
+ if (
+ isVisible &&
+ (bannerMetrics.bannerVisibleTime === 0 ||
+ now < bannerMetrics.bannerVisibleTime)
+ ) {
+ bannerMetrics.bannerVisibleTime = now;
+ }
+
+ // Check if banner is interactive
+ const buttons = element.querySelectorAll(
+ 'button, a, [role="button"], [onclick]'
+ );
+ if (buttons.length > 0) {
+ // Test if buttons are actually clickable
+ const firstButton = buttons[0] as HTMLElement;
+ if (firstButton.offsetParent !== null) {
+ // Element is visible and clickable
+ bannerMetrics.bannerInteractive = now;
+ }
+ }
+
+ return true;
+ }
+ }
+ } catch (_error) {
+ // Ignore selector errors and continue checking other selectors
+ }
+ }
+ return false;
+ };
+
+ // Start detection immediately - try right away, then poll if needed
+ // This ensures we catch banners that appear instantly (like in offline/bundled mode)
+ const startDetection = () => {
+ // Try immediate detection first (catches instant renders)
+ if (!detectCookieBanner()) {
+ // If not found immediately, start polling
+ const interval = setInterval(() => {
+ if (detectCookieBanner()) {
+ clearInterval(interval);
+ }
+ }, pollInterval);
+
+ // Stop checking after timeout
+ setTimeout(() => clearInterval(interval), detectionTimeout);
+ }
+ };
+
+ // Always start immediately to catch banners rendered very early.
+ startDetection();
+ if (document.readyState === "loading") {
+ // Run one more detection pass once DOM is fully parsed.
+ document.addEventListener("DOMContentLoaded", startDetection, {
+ once: true,
+ });
+ }
+ },
+ {
+ bannerSelectors: selectors,
+ pollInterval: BENCHMARK_CONSTANTS.BANNER_POLL_INTERVAL,
+ detectionTimeout: BENCHMARK_CONSTANTS.BANNER_DETECTION_TIMEOUT,
+ opacityThreshold: OPACITY_VISIBILITY_THRESHOLD,
+ }
+ );
+ }
+
+ /**
+ * Collect cookie banner specific metrics from the browser.
+ *
+ * Returns both technical metrics (bannerRenderTime) and user-perceived metrics
+ * (bannerVisibilityTime). The visibilityTime metric is used for scoring as it
+ * reflects actual user experience including CSS animations.
+ *
+ * All timing metrics are relative to navigationStart (includes TTFB).
+ *
+ * @returns CookieBannerData with render time, visibility time, interactive time,
+ * layout shift impact, and viewport coverage
+ * @see METHODOLOGY.md for detailed explanation of metrics
+ */
+ async collectMetrics(page: Page): Promise {
+ return await page.evaluate(
+ (config: { percentageMultiplier: number }) => {
+ const { percentageMultiplier } = config;
+ const metrics = (window as unknown as WindowWithCookieMetrics)
+ .__cookieBannerMetrics;
+ if (!metrics) {
+ return null;
+ }
+
+ // Read final layout shift value before disconnecting observer
+ const layoutShiftImpact =
+ metrics.layoutShiftsAfter - metrics.layoutShiftsBefore;
+
+ // Clean up PerformanceObserver to prevent memory leaks
+ if (metrics.clsObserver) {
+ metrics.clsObserver.disconnect();
+ metrics.clsObserver = undefined;
+ }
+
+ const domPresenceTime =
+ metrics.detected && metrics.bannerFirstSeen > 0
+ ? metrics.bannerFirstSeen - metrics.pageLoadStart
+ : 0;
+ const userVisibleTime = (() => {
+ if (metrics.detected && metrics.bannerVisibleTime > 0) {
+ return metrics.bannerVisibleTime - metrics.pageLoadStart;
+ }
+ if (metrics.detected && metrics.bannerFirstSeen > 0) {
+ return metrics.bannerFirstSeen - metrics.pageLoadStart;
+ }
+ return 0;
+ })();
+
+ return {
+ detected: metrics.detected,
+ selector: metrics.selector,
+ bannerRenderTime: domPresenceTime,
+ bannerVisibilityTime: userVisibleTime,
+ domPresenceTime,
+ userVisibleTime,
+ bannerInteractiveTime:
+ metrics.detected && metrics.bannerInteractive > 0
+ ? metrics.bannerInteractive - metrics.pageLoadStart
+ : 0,
+ bannerHydrationTime:
+ metrics.detected && metrics.bannerInteractive > 0
+ ? metrics.bannerInteractive - metrics.bannerFirstSeen
+ : 0,
+ layoutShiftImpact,
+ viewportCoverage: metrics.detected
+ ? (() => {
+ if (!metrics.selector) {
+ return 0;
+ }
+ const element = document.querySelector(metrics.selector);
+ if (element) {
+ const rect = element.getBoundingClientRect();
+ const viewportWidth = window.innerWidth;
+ const viewportHeight = window.innerHeight;
+
+ // Calculate the intersection of element rect with viewport
+ // Only count the visible portion of the banner
+ const visibleLeft = Math.max(0, rect.left);
+ const visibleTop = Math.max(0, rect.top);
+ const visibleRight = Math.min(
+ viewportWidth,
+ rect.left + rect.width
+ );
+ const visibleBottom = Math.min(
+ viewportHeight,
+ rect.top + rect.height
+ );
+
+ // Calculate visible area (intersection with viewport)
+ const visibleWidth = Math.max(0, visibleRight - visibleLeft);
+ const visibleHeight = Math.max(0, visibleBottom - visibleTop);
+ const visibleArea = visibleWidth * visibleHeight;
+ const viewportArea = viewportWidth * viewportHeight;
+
+ if (viewportArea === 0) {
+ return 0;
+ }
+
+ return (visibleArea / viewportArea) * percentageMultiplier;
+ }
+ return 0;
+ })()
+ : 0,
+ };
+ },
+ { percentageMultiplier: BENCHMARK_CONSTANTS.PERCENTAGE_MULTIPLIER }
+ );
+ }
+}
diff --git a/packages/benchmark/src/index.ts b/packages/benchmark/src/index.ts
new file mode 100644
index 0000000..58713c5
--- /dev/null
+++ b/packages/benchmark/src/index.ts
@@ -0,0 +1,27 @@
+/** biome-ignore-all lint/performance/noBarrelFile: this is a barrel file */
+
+// Utilities
+export { determineBundleStrategy } from "./bundle-strategy";
+export { BENCHMARK_CONSTANTS, BUNDLE_TYPES } from "./constants";
+export { CookieBannerCollector } from "./cookie-banner-collector";
+export { NetworkMonitor } from "./network-monitor";
+export { PerfumeCollector } from "./perfume-collector";
+export { ResourceTimingCollector } from "./resource-timing-collector";
+
+// Types
+export type {
+ BundleType,
+ BundleStrategy,
+ Config,
+ CookieBannerConfig,
+ CookieBannerData,
+ CookieBannerMetrics,
+ CoreWebVitals,
+ LayoutShiftEntry,
+ NetworkMetrics,
+ NetworkRequest,
+ PerfumeMetrics,
+ ResourceTimingData,
+ WindowWithCookieMetrics,
+ WindowWithPerfumeMetrics,
+} from "./types";
diff --git a/packages/benchmark/src/network-monitor.ts b/packages/benchmark/src/network-monitor.ts
new file mode 100644
index 0000000..4575e01
--- /dev/null
+++ b/packages/benchmark/src/network-monitor.ts
@@ -0,0 +1,135 @@
+import type { Logger } from "@c15t/logger";
+import type { Page, Route } from "@playwright/test";
+import { BENCHMARK_CONSTANTS } from "./constants";
+import type { Config, NetworkMetrics, NetworkRequest } from "./types";
+
+export class NetworkMonitor {
+ private readonly config: Config;
+ private readonly logger: Logger;
+ private networkRequests: NetworkRequest[] = [];
+ private metrics: NetworkMetrics = {
+ bannerNetworkRequests: 0,
+ bannerBundleSize: 0,
+ };
+
+ constructor(config: Config, logger: Logger) {
+ this.config = config;
+ this.logger = logger;
+ }
+
+ /**
+ * Set up network request monitoring
+ */
+ async setupMonitoring(page: Page, targetUrl?: string): Promise {
+ // Extract first-party hostname from config, provided URL, or page URL
+ const firstPartyUrl = this.config.url || targetUrl || page.url();
+ const firstPartyHostname = new URL(firstPartyUrl).hostname;
+
+ await page.route("**/*", async (route: Route) => {
+ const request = route.request();
+ const url = request.url();
+ const startTime = Date.now();
+
+ try {
+ const response = await route.fetch();
+ const headers = response.headers();
+
+ // Add timing-allow-origin header for all responses
+ headers["timing-allow-origin"] = "*";
+
+ const isScript = request.resourceType() === "script";
+ // Compare request hostname against first-party hostname for third-party detection
+ const requestHostname = new URL(url).hostname;
+ const isThirdParty = requestHostname !== firstPartyHostname;
+
+ if (isScript) {
+ const contentLength = response.headers()["content-length"];
+ const size = contentLength ? +contentLength || 0 : 0;
+
+ // Calculate duration from request start to response
+ const duration = Date.now() - startTime;
+
+ this.networkRequests.push({
+ url,
+ size: size / BENCHMARK_CONSTANTS.BYTES_TO_KB, // Convert to KB
+ duration,
+ startTime,
+ isScript,
+ isThirdParty,
+ });
+
+ if (isThirdParty) {
+ this.metrics.bannerNetworkRequests += 1;
+ this.metrics.bannerBundleSize +=
+ size / BENCHMARK_CONSTANTS.BYTES_TO_KB;
+ this.logger.debug(
+ `Third-party script detected: ${url} (${(size / BENCHMARK_CONSTANTS.BYTES_TO_KB).toFixed(2)}KB)`
+ );
+ }
+ }
+
+ await route.fulfill({ response, headers });
+ } catch {
+ // If we can't modify the response, just continue with the original request
+ await route.continue();
+ }
+ });
+ }
+
+ /**
+ * Get collected network requests
+ */
+ getNetworkRequests(): NetworkRequest[] {
+ return this.networkRequests;
+ }
+
+ /**
+ * Get network metrics
+ */
+ getMetrics(): NetworkMetrics {
+ return this.metrics;
+ }
+
+ /**
+ * Calculate network impact metrics
+ */
+ calculateNetworkImpact(): {
+ totalImpact: number;
+ totalDownloadTime: number;
+ thirdPartyImpact: number;
+ scriptImpact: number;
+ } {
+ const totalImpact = this.networkRequests.reduce(
+ (acc, req) => acc + req.size,
+ 0
+ );
+ const totalDownloadTime = this.networkRequests.reduce(
+ (acc, req) => acc + req.duration,
+ 0
+ );
+ const thirdPartyImpact = this.networkRequests
+ .filter((req) => req.isThirdParty)
+ .reduce((acc, req) => acc + req.size, 0);
+ const scriptImpact = this.networkRequests
+ .filter((req) => req.isScript)
+ .reduce((acc, req) => acc + req.size, 0);
+
+ return {
+ totalImpact,
+ totalDownloadTime,
+ thirdPartyImpact,
+ scriptImpact,
+ };
+ }
+
+ /**
+ * Reset collected data
+ */
+ reset(): void {
+ this.networkRequests = [];
+ this.metrics = {
+ bannerNetworkRequests: 0,
+ bannerBundleSize: 0,
+ };
+ }
+}
diff --git a/packages/benchmark/src/perfume-collector.ts b/packages/benchmark/src/perfume-collector.ts
new file mode 100644
index 0000000..fa7e074
--- /dev/null
+++ b/packages/benchmark/src/perfume-collector.ts
@@ -0,0 +1,261 @@
+import { readFileSync } from "node:fs";
+import { createRequire } from "node:module";
+import { join } from "node:path";
+import { fileURLToPath } from "node:url";
+import type { Logger } from "@c15t/logger";
+import type { Page } from "@playwright/test";
+import { BENCHMARK_CONSTANTS } from "./constants";
+import type { PerfumeMetrics, WindowWithPerfumeMetrics } from "./types";
+
+export class PerfumeCollector {
+ private readonly logger: Logger;
+
+ constructor(logger: Logger) {
+ this.logger = logger;
+ }
+ /**
+ * Setup Perfume.js in the browser to collect performance metrics
+ */
+ async setupPerfume(page: Page): Promise {
+ // Load Perfume.js UMD bundle from node_modules
+ const perfumeScript = this.loadPerfumeScript();
+
+ await page.addInitScript((perfumeScriptCode: string) => {
+ // Initialize storage object
+ const win = window as WindowWithPerfumeMetrics;
+ win.__perfumeMetrics = {};
+
+ if (!perfumeScriptCode) {
+ // Perfume.js failed to load, continue without it
+ return;
+ }
+
+ try {
+ // Execute Perfume.js UMD bundle using script injection
+ // This creates the Perfume constructor on window
+ const script = document.createElement("script");
+ script.textContent = perfumeScriptCode;
+ document.head.appendChild(script);
+ document.head.removeChild(script);
+
+ // Initialize Perfume with analytics tracker
+ // @ts-expect-error - Perfume is loaded from UMD bundle
+ new window.Perfume({
+ analyticsTracker: ({
+ metricName,
+ data,
+ rating,
+ attribution,
+ navigatorInformation,
+ }: {
+ metricName: string;
+ data: number;
+ rating: string;
+ attribution?: unknown;
+ navigatorInformation?: {
+ deviceMemory?: number;
+ hardwareConcurrency?: number;
+ isLowEndDevice?: boolean;
+ isLowEndExperience?: boolean;
+ serviceWorkerStatus?: string;
+ };
+ }) => {
+ const metricsWin = window as WindowWithPerfumeMetrics;
+ const metrics = metricsWin.__perfumeMetrics;
+
+ // Store metric with all available data
+ if (metrics) {
+ metrics[metricName] = {
+ value: data,
+ rating,
+ attribution,
+ navigatorInformation,
+ };
+ }
+ },
+ });
+ } catch (error) {
+ // biome-ignore lint/suspicious/noConsole: This runs in browser context via addInitScript
+ console.warn("Failed to initialize Perfume.js:", error);
+ // Perfume.js is optional, continue without it
+ }
+ }, perfumeScript);
+ }
+
+ /**
+ * Load Perfume.js UMD bundle from node_modules
+ */
+ private loadPerfumeScript(): string {
+ // Try multiple possible paths for perfume.js in a monorepo setup
+ const currentDir = fileURLToPath(new URL(".", import.meta.url));
+ const possiblePaths = [
+ // Package-level node_modules (most common in monorepos)
+ join(
+ currentDir,
+ "..",
+ "..",
+ "node_modules",
+ "perfume.js",
+ "dist",
+ "perfume.umd.min.js"
+ ),
+ // Root node_modules (alternative location)
+ join(
+ currentDir,
+ "..",
+ "..",
+ "..",
+ "..",
+ "node_modules",
+ "perfume.js",
+ "dist",
+ "perfume.umd.min.js"
+ ),
+ // Alternative root location
+ join(
+ currentDir,
+ "..",
+ "..",
+ "..",
+ "node_modules",
+ "perfume.js",
+ "dist",
+ "perfume.umd.min.js"
+ ),
+ // Also try non-minified version
+ join(
+ currentDir,
+ "..",
+ "..",
+ "node_modules",
+ "perfume.js",
+ "dist",
+ "perfume.umd.js"
+ ),
+ ];
+
+ for (const perfumePath of possiblePaths) {
+ try {
+ return readFileSync(perfumePath, "utf-8");
+ } catch {
+ // Try next path
+ }
+ }
+
+ // Try using createRequire for ES modules (works in Node.js environments)
+ try {
+ const require = createRequire(import.meta.url);
+ const perfumeModulePath = require.resolve(
+ "perfume.js/dist/perfume.umd.min.js"
+ );
+ return readFileSync(perfumeModulePath, "utf-8");
+ } catch {
+ // Final fallback
+ }
+
+ this.logger.warn(
+ "Failed to load Perfume.js from node_modules, falling back to empty script"
+ );
+ return "";
+ }
+
+ /**
+ * Collect all metrics from Perfume.js
+ */
+ async collectMetrics(page: Page): Promise {
+ try {
+ // Wait a bit for metrics to be collected
+ await page.waitForTimeout(BENCHMARK_CONSTANTS.PERFUME_METRICS_WAIT);
+
+ const rawMetrics = await page.evaluate(() => {
+ const win = window as WindowWithPerfumeMetrics;
+ const perfumeData = win.__perfumeMetrics;
+ return perfumeData || {};
+ });
+
+ this.logger.debug("Raw Perfume metrics:", rawMetrics);
+
+ // Get navigation timing separately
+ const navigationTiming = await page.evaluate(() => {
+ const navigation = performance.getEntriesByType("navigation")[0] as
+ | PerformanceNavigationTiming
+ | undefined;
+
+ if (!navigation) {
+ return null;
+ }
+
+ return {
+ timeToFirstByte: navigation.responseStart,
+ domInteractive: navigation.domInteractive,
+ domContentLoadedEventStart: navigation.domContentLoadedEventStart,
+ domContentLoadedEventEnd: navigation.domContentLoadedEventEnd,
+ domComplete: navigation.domComplete,
+ loadEventStart: navigation.loadEventStart,
+ loadEventEnd: navigation.loadEventEnd,
+ };
+ });
+
+ // Get network information
+ const networkInformation = await page.evaluate(() => {
+ try {
+ const nav = typeof navigator !== "undefined" ? navigator : null;
+ if (!nav) {
+ return;
+ }
+
+ // Access experimental network information API with vendor prefixes
+ // biome-ignore lint/suspicious/noExplicitAny: Experimental API requires dynamic access
+ const navAny = nav as any;
+ const connection =
+ navAny.connection ||
+ navAny.mozConnection ||
+ navAny.webkitConnection;
+
+ if (connection) {
+ return {
+ effectiveType: connection.effectiveType || "unknown",
+ downlink: connection.downlink || 0,
+ rtt: connection.rtt || 0,
+ saveData: Boolean(connection.saveData),
+ };
+ }
+
+ return;
+ } catch {
+ return;
+ }
+ });
+
+ // Convert raw metrics to PerfumeMetrics format
+ const defaultNavigationTiming = {
+ timeToFirstByte: 0,
+ domInteractive: 0,
+ domContentLoadedEventStart: 0,
+ domContentLoadedEventEnd: 0,
+ domComplete: 0,
+ loadEventStart: 0,
+ loadEventEnd: 0,
+ };
+
+ const metrics: PerfumeMetrics = {
+ firstPaint: rawMetrics.fp?.value || 0,
+ firstContentfulPaint: rawMetrics.FCP?.value || 0,
+ largestContentfulPaint: rawMetrics.LCP?.value || 0,
+ cumulativeLayoutShift: rawMetrics.CLS?.value || 0,
+ totalBlockingTime: rawMetrics.TBT?.value || 0,
+ firstInputDelay: rawMetrics.FID?.value ?? null,
+ interactionToNextPaint: rawMetrics.INP?.value ?? null,
+ timeToFirstByte:
+ rawMetrics.TTFB?.value || navigationTiming?.timeToFirstByte || 0,
+ navigationTiming: navigationTiming || defaultNavigationTiming,
+ networkInformation,
+ };
+
+ return metrics;
+ } catch (error) {
+ this.logger.error("Failed to collect Perfume metrics:", error);
+ return null;
+ }
+ }
+}
diff --git a/packages/benchmark/src/resource-timing-collector.ts b/packages/benchmark/src/resource-timing-collector.ts
new file mode 100644
index 0000000..b3f180e
--- /dev/null
+++ b/packages/benchmark/src/resource-timing-collector.ts
@@ -0,0 +1,188 @@
+import type { Logger } from "@c15t/logger";
+import type { Page } from "@playwright/test";
+import { BENCHMARK_CONSTANTS } from "./constants";
+import type { ResourceTimingData } from "./types";
+
+export class ResourceTimingCollector {
+ private readonly logger: Logger;
+
+ constructor(logger: Logger) {
+ this.logger = logger;
+ }
+ /**
+ * Collect detailed resource timing data from the browser
+ */
+ async collect(page: Page): Promise {
+ this.logger.debug("Collecting resource timing data...");
+
+ return await page.evaluate((bytesToKb: number) => {
+ const perfEntries = performance.getEntriesByType(
+ "navigation"
+ )[0] as PerformanceNavigationTiming;
+ const resourceEntries = performance.getEntriesByType(
+ "resource"
+ ) as PerformanceResourceTiming[];
+
+ // Helper to determine if a resource is first-party
+ const isFirstParty = (entry: PerformanceResourceTiming) => {
+ try {
+ return (
+ new URL(entry.name, window.location.origin).hostname ===
+ window.location.hostname
+ );
+ } catch {
+ return (
+ entry.name.startsWith(window.location.origin) ||
+ entry.name.startsWith("/")
+ );
+ }
+ };
+
+ // Categorize resources
+ const scriptEntries = resourceEntries.filter(
+ (entry) => entry.initiatorType === "script"
+ );
+ const styleEntries = resourceEntries.filter(
+ (entry) => entry.initiatorType === "link" && entry.name.endsWith(".css")
+ );
+ const imageEntries = resourceEntries.filter(
+ (entry) => entry.initiatorType === "img"
+ );
+ const fontEntries = resourceEntries.filter(
+ (entry) => entry.initiatorType === "font"
+ );
+ const otherEntries = resourceEntries.filter(
+ (entry) =>
+ !["script", "link", "img", "font"].includes(entry.initiatorType)
+ );
+
+ // Calculate sizes
+ const calculateSize = (entries: PerformanceResourceTiming[]) => {
+ const total =
+ entries.reduce((acc, entry) => {
+ const size = entry.transferSize || entry.encodedBodySize || 0;
+ return acc + size;
+ }, 0) / bytesToKb;
+ return total;
+ };
+
+ const navigationStart = perfEntries.startTime;
+ const domContentLoaded =
+ perfEntries.domContentLoadedEventEnd - navigationStart;
+ const load = perfEntries.loadEventEnd - navigationStart;
+
+ return {
+ timing: {
+ navigationStart,
+ domContentLoaded,
+ load,
+ scripts: {
+ bundled: {
+ loadStart: 0,
+ loadEnd: scriptEntries
+ .filter((entry) => isFirstParty(entry))
+ .reduce((acc, entry) => acc + entry.duration, 0),
+ executeStart: 0,
+ executeEnd: 0,
+ },
+ thirdParty: {
+ loadStart: 0,
+ loadEnd: scriptEntries
+ .filter((entry) => !isFirstParty(entry))
+ .reduce((acc, entry) => acc + entry.duration, 0),
+ executeStart: 0,
+ executeEnd: 0,
+ },
+ },
+ },
+ size: {
+ total: calculateSize(resourceEntries),
+ bundled: calculateSize(
+ scriptEntries.filter((entry) => isFirstParty(entry))
+ ),
+ thirdParty: calculateSize(
+ scriptEntries.filter((entry) => !isFirstParty(entry))
+ ),
+ cookieServices: 0, // Will be calculated later
+ scripts: {
+ total: calculateSize(scriptEntries),
+ initial: calculateSize(
+ scriptEntries.filter((e) => e.startTime < domContentLoaded)
+ ),
+ dynamic: calculateSize(
+ scriptEntries.filter((e) => e.startTime >= domContentLoaded)
+ ),
+ thirdParty: calculateSize(
+ scriptEntries.filter((entry) => !isFirstParty(entry))
+ ),
+ cookieServices: 0, // Will be calculated later
+ },
+ styles: calculateSize(styleEntries),
+ images: calculateSize(imageEntries),
+ fonts: calculateSize(fontEntries),
+ other: calculateSize(otherEntries),
+ },
+ resources: {
+ scripts: scriptEntries.map((entry) => ({
+ name: entry.name,
+ size:
+ (entry.transferSize || entry.encodedBodySize || 0) / bytesToKb,
+ duration: entry.duration,
+ startTime: entry.startTime - navigationStart,
+ isThirdParty: !isFirstParty(entry),
+ isDynamic: entry.startTime >= domContentLoaded,
+ isCookieService: false,
+ dnsTime: entry.domainLookupEnd - entry.domainLookupStart,
+ connectionTime: entry.connectEnd - entry.connectStart,
+ })),
+ styles: styleEntries.map((entry) => ({
+ name: entry.name,
+ size:
+ (entry.transferSize || entry.encodedBodySize || 0) / bytesToKb,
+ duration: entry.duration,
+ startTime: entry.startTime - navigationStart,
+ isThirdParty: !isFirstParty(entry),
+ isCookieService: false,
+ })),
+ images: imageEntries.map((entry) => ({
+ name: entry.name,
+ size:
+ (entry.transferSize || entry.encodedBodySize || 0) / bytesToKb,
+ duration: entry.duration,
+ startTime: entry.startTime - navigationStart,
+ isThirdParty: !isFirstParty(entry),
+ isCookieService: false,
+ })),
+ fonts: fontEntries.map((entry) => ({
+ name: entry.name,
+ size:
+ (entry.transferSize || entry.encodedBodySize || 0) / bytesToKb,
+ duration: entry.duration,
+ startTime: entry.startTime - navigationStart,
+ isThirdParty: !isFirstParty(entry),
+ isCookieService: false,
+ })),
+ other: otherEntries.map((entry) => ({
+ name: entry.name,
+ size:
+ (entry.transferSize || entry.encodedBodySize || 0) / bytesToKb,
+ duration: entry.duration,
+ startTime: entry.startTime - navigationStart,
+ isThirdParty: !isFirstParty(entry),
+ isCookieService: false,
+ type: entry.initiatorType,
+ })),
+ },
+ language: (() => {
+ const docLang = (
+ document.documentElement.getAttribute("lang") || ""
+ ).trim();
+ return (
+ docLang || navigator.language || navigator.languages?.[0] || "en"
+ );
+ })(),
+ duration: load,
+ };
+ }, BENCHMARK_CONSTANTS.BYTES_TO_KB);
+ }
+}
diff --git a/packages/benchmark/src/types.ts b/packages/benchmark/src/types.ts
new file mode 100644
index 0000000..230772e
--- /dev/null
+++ b/packages/benchmark/src/types.ts
@@ -0,0 +1,288 @@
+import type { Page } from "@playwright/test";
+
+// Config types
+export type CookieBannerConfig = {
+ selectors: string[];
+ serviceHosts: string[];
+ waitForVisibility: boolean;
+ measureViewportCoverage: boolean;
+ expectedLayoutShift: boolean;
+ serviceName: string;
+};
+
+export type BundleType = "esm" | "cjs" | "iffe" | "bundled";
+
+export type Config = {
+ name: string;
+ url?: string;
+ testId?: string;
+ id?: string;
+ iterations: number;
+ baseline?: boolean;
+ custom?: (page: Page) => Promise;
+ remote?: {
+ enabled?: boolean;
+ url?: string;
+ headers?: Record;
+ };
+ cookieBanner: CookieBannerConfig;
+ internationalization: {
+ detection: string;
+ stringLoading: string;
+ };
+ techStack: {
+ bundler: string;
+ bundleType: BundleType | BundleType[];
+ frameworks: string[];
+ languages: string[];
+ packageManager: string;
+ typescript: boolean;
+ };
+ source: {
+ github: string | false;
+ isOpenSource: boolean | string;
+ license: string;
+ npm: string | false;
+ website?: string;
+ };
+ includes: {
+ backend: string | string[] | false;
+ components: string[];
+ };
+ company?: {
+ name: string;
+ website: string;
+ avatar: string;
+ };
+ tags?: string[];
+};
+
+// Performance API type definitions
+export interface LayoutShiftEntry extends PerformanceEntry {
+ value: number;
+ hadRecentInput: boolean;
+}
+
+// Cookie banner types
+export interface WindowWithCookieMetrics extends Window {
+ __cookieBannerMetrics: {
+ pageLoadStart: number;
+ bannerDetectionStart: number;
+ bannerFirstSeen: number;
+ bannerVisibleTime: number; // When banner is actually visible (opacity > 0.5) - for UX metrics
+ bannerInteractive: number;
+ layoutShiftsBefore: number;
+ layoutShiftsAfter: number;
+ detected: boolean;
+ selector: string | null;
+ clsObserver?: PerformanceObserver;
+ };
+}
+
+export type CookieBannerMetrics = {
+ detectionStartTime: number;
+ bannerRenderTime: number;
+ bannerInteractiveTime: number;
+ bannerScriptLoadTime: number;
+ bannerLayoutShiftImpact: number;
+ bannerNetworkRequests: number;
+ bannerBundleSize: number;
+ bannerMainThreadBlockingTime: number;
+ isBundled: boolean;
+ isIIFE: boolean;
+ bannerDetected: boolean;
+ bannerSelector: string | null;
+};
+
+export type CookieBannerData = {
+ detected: boolean;
+ selector: string | null;
+ /** DOM presence time (ms): when banner is first painted to screen (technical render). */
+ bannerRenderTime: number;
+ /** User-visible time (ms): when banner is actually visible to users (opacity > 0.5). Used for scoring. */
+ bannerVisibilityTime: number;
+ /** Explicit alias for downstream; same as bannerRenderTime. */
+ domPresenceTime?: number;
+ /** Explicit alias for downstream; same as bannerVisibilityTime. */
+ userVisibleTime?: number;
+ bannerInteractiveTime: number;
+ bannerHydrationTime: number;
+ layoutShiftImpact: number;
+ viewportCoverage: number;
+};
+
+// Network types
+export type NetworkRequest = {
+ url: string;
+ size: number;
+ duration: number;
+ startTime: number;
+ isScript: boolean;
+ isThirdParty: boolean;
+};
+
+export type NetworkMetrics = {
+ bannerNetworkRequests: number;
+ bannerBundleSize: number;
+};
+
+// Bundle strategy types
+export type BundleStrategy = {
+ isBundled: boolean;
+ isIIFE: boolean;
+ bundleType: BundleType | BundleType[] | undefined;
+};
+
+// Resource timing types
+export type ResourceTimingData = {
+ timing: {
+ navigationStart: number;
+ domContentLoaded: number;
+ load: number;
+ scripts: {
+ bundled: {
+ loadStart: number;
+ loadEnd: number;
+ executeStart: number;
+ executeEnd: number;
+ };
+ thirdParty: {
+ loadStart: number;
+ loadEnd: number;
+ executeStart: number;
+ executeEnd: number;
+ };
+ };
+ };
+ size: {
+ total: number;
+ bundled: number;
+ thirdParty: number;
+ cookieServices: number;
+ scripts: {
+ total: number;
+ initial: number;
+ dynamic: number;
+ thirdParty: number;
+ cookieServices: number;
+ };
+ styles: number;
+ images: number;
+ fonts: number;
+ other: number;
+ };
+ resources: {
+ scripts: Array<{
+ name: string;
+ size: number;
+ duration: number;
+ startTime: number;
+ isThirdParty: boolean;
+ isDynamic: boolean;
+ isCookieService: boolean;
+ dnsTime: number;
+ connectionTime: number;
+ }>;
+ styles: Array<{
+ name: string;
+ size: number;
+ duration: number;
+ startTime: number;
+ isThirdParty: boolean;
+ isCookieService: boolean;
+ }>;
+ images: Array<{
+ name: string;
+ size: number;
+ duration: number;
+ startTime: number;
+ isThirdParty: boolean;
+ isCookieService: boolean;
+ }>;
+ fonts: Array<{
+ name: string;
+ size: number;
+ duration: number;
+ startTime: number;
+ isThirdParty: boolean;
+ isCookieService: boolean;
+ }>;
+ other: Array<{
+ name: string;
+ size: number;
+ duration: number;
+ startTime: number;
+ isThirdParty: boolean;
+ isCookieService: boolean;
+ type: string;
+ }>;
+ };
+ language: string;
+ duration: number;
+};
+
+// Core web vitals types
+export type CoreWebVitals = {
+ paint?: {
+ firstPaint?: number;
+ firstContentfulPaint?: number;
+ };
+ largestContentfulPaint?: number;
+ cumulativeLayoutShift?: number;
+ totalBlockingTime?: number;
+ domCompleteTiming?: number;
+ pageloadTiming?: number;
+ totalBytes?: number;
+};
+
+// Perfume.js metrics types
+export type PerfumeMetrics = {
+ // Core Web Vitals (replacing playwright-performance-metrics)
+ firstPaint: number;
+ firstContentfulPaint: number;
+ largestContentfulPaint: number;
+ cumulativeLayoutShift: number;
+ totalBlockingTime: number;
+
+ // Enhanced metrics (new)
+ firstInputDelay: number | null;
+ interactionToNextPaint: number | null;
+ timeToFirstByte: number;
+
+ // Detailed navigation timing
+ navigationTiming: {
+ timeToFirstByte: number;
+ domInteractive: number;
+ domContentLoadedEventStart: number;
+ domContentLoadedEventEnd: number;
+ domComplete: number;
+ loadEventStart: number;
+ loadEventEnd: number;
+ };
+
+ // Network information (optional)
+ networkInformation?: {
+ effectiveType: string;
+ downlink: number;
+ rtt: number;
+ saveData: boolean;
+ };
+};
+
+export interface WindowWithPerfumeMetrics extends Window {
+ __perfumeMetrics?: Record<
+ string,
+ {
+ value: number;
+ rating: string;
+ attribution?: unknown;
+ navigatorInformation?: {
+ deviceMemory?: number;
+ hardwareConcurrency?: number;
+ isLowEndDevice?: boolean;
+ isLowEndExperience?: boolean;
+ serviceWorkerStatus?: string;
+ };
+ }
+ >;
+}
diff --git a/packages/benchmark/tsconfig.json b/packages/benchmark/tsconfig.json
new file mode 100644
index 0000000..fde9b9d
--- /dev/null
+++ b/packages/benchmark/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "esModuleInterop": true,
+ "strict": true,
+ "skipLibCheck": true,
+ "declaration": true,
+ "outDir": "dist",
+ "rootDir": "src"
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/packages/cli/package.json b/packages/cli/package.json
index 38f70f7..08c461f 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -19,17 +19,17 @@
},
"dependencies": {
"@clack/prompts": "1.0.0-alpha.6",
- "@playwright/test": "^1.57.0",
+ "@playwright/test": "^1.58.2",
"cli-table3": "^0.6.5",
- "dotenv": "^17.2.3",
+ "dotenv": "^17.3.1",
"package-manager-detector": "^1.6.0",
"picocolors": "^1.1.1"
},
"devDependencies": {
- "@rsdoctor/rspack-plugin": "^1.3.12",
- "@rslib/core": "^0.18.3",
- "@types/node": "^24.10.1",
- "playwright-performance-metrics": "^1.2.4",
- "typescript": "^5.9.3"
+ "@rsdoctor/rspack-plugin": "^1.5.2",
+ "@rslib/core": "^0.18.6",
+ "@types/node": "catalog:",
+ "playwright-performance-metrics": "^1.2.5",
+ "typescript": "catalog:"
}
}
\ No newline at end of file
diff --git a/packages/cookiebench-cli/README.md b/packages/cookiebench-cli/README.md
new file mode 100644
index 0000000..45294f1
--- /dev/null
+++ b/packages/cookiebench-cli/README.md
@@ -0,0 +1,399 @@
+# cookiebench
+
+Command-line interface for cookie banner performance benchmarking.
+
+## Overview
+
+This CLI tool provides an interactive interface for running cookie banner benchmarks, viewing results, and managing the benchmark database.
+
+## Installation
+
+```bash
+pnpm add -g cookiebench
+```
+
+Or use in a workspace:
+
+```bash
+pnpm add cookiebench
+```
+
+## Usage
+
+### Interactive Mode
+
+Run without arguments for interactive prompts:
+
+```bash
+cookiebench
+```
+
+### Direct Commands
+
+Run specific commands directly:
+
+```bash
+# Run a benchmark
+cookiebench benchmark
+
+# View detailed benchmark results
+cookiebench results
+
+# View scores from existing results (available via CLI only)
+cookiebench scores
+
+# Sync results to database (admin only)
+cookiebench save
+
+# Manage database (admin only)
+cookiebench db push
+```
+
+## Commands
+
+### benchmark
+
+Run performance benchmarks on configured applications.
+
+```bash
+# Interactive mode - select multiple benchmarks to run
+cookiebench benchmark
+
+# Run a specific benchmark
+cookiebench benchmark benchmarks/c15t-nextjs
+```
+
+**Interactive Multi-Select Mode:**
+
+When run without arguments, the command will:
+1. Scan the `benchmarks/` directory for available benchmarks
+2. Present a multi-select interface (use space to toggle selections)
+3. Show iteration counts from each config file
+4. Ask for iterations override (or press Enter to use config values)
+5. Ask if you want to show the results panel after completion
+6. Run selected benchmarks sequentially
+7. Show a summary of completed benchmarks
+8. Automatically display results for the benchmarks you just ran (if enabled, skips selection menu)
+
+**Single Benchmark Mode:**
+
+When a specific path is provided:
+1. Read `config.json` from the specified path
+2. Build and serve the Next.js app (or use remote URL if configured)
+3. Run benchmarks for the specified number of iterations
+4. Calculate performance scores
+5. Save results to `results.json`
+6. Display scores
+
+**Features:**
+- ✅ Multi-select with space bar toggle
+- ✅ View default iterations from config files
+- ✅ Override iterations for all benchmarks or use individual config values
+- ✅ Sequential execution with progress indicators
+- ✅ Automatic results panel for completed benchmarks (no extra selection needed)
+- ✅ Comprehensive metrics: scores, insights, Core Web Vitals, resource breakdown, network waterfall
+- ✅ Error handling - continues to next benchmark on failure
+- ✅ Summary report at the end
+
+**Example:**
+
+```text
+? Select benchmarks to run:
+ ◼ baseline
+ ◼ c15t-nextjs
+ ◼ cookieyes
+
+● info Config iterations: baseline: 5, c15t-nextjs: 3, cookieyes: 5
+
+? Number of iterations (press Enter to use config values):
+› Default: 5
+
+ [Just press Enter to use config values, or type a number to override all]
+
+? Show results panel after completion? › Yes
+
+```
+
+### results
+
+View comprehensive benchmark results with detailed metrics and analysis.
+
+```bash
+# Interactive mode - select which benchmarks to view (all selected by default)
+cookiebench results
+
+# View results for a specific app
+cookiebench results c15t-nextjs
+
+# View all results
+cookiebench results __all__
+```
+
+**Interactive Multi-Select Mode:**
+
+When run without arguments, you can:
+1. Select which benchmarks to view using space bar (all selected by default)
+2. Press Enter to view detailed results for selected benchmarks
+3. View baseline comparisons and deltas
+
+**Features:**
+
+Displays a detailed panel for each selected benchmark with:
+
+1. **🎯 Overall Score** - Color-coded score (0-100) with grade (Excellent/Good/Fair/Poor/Critical)
+ - 🟢 Green (90+) = Excellent
+ - 🟡 Yellow (70-89) = Good/Fair
+ - 🔴 Red (<70) = Poor/Critical
+
+2. **💡 Key Insights** - Auto-generated bullet points highlighting strengths and areas for improvement
+
+3. **🍪 Cookie Banner Impact** - Banner visibility, viewport coverage, network impact, bundle strategy
+
+4. **⚡ Core Web Vitals** - FCP, LCP, TTI, CLS with performance ratings
+
+5. **📦 Resource Breakdown** - Detailed size analysis by type (JS, CSS, Images, Fonts, Other)
+
+6. **📊 Performance Impact Summary** - Loading strategy, render performance, network overhead, layout stability
+
+7. **🌐 Network Chart** - ASCII waterfall visualization showing resource loading timeline with color-coded bars
+
+8. **📋 Resource Details** - Sortable table with resource names, types, sources, sizes, and durations
+
+**Notes:**
+- Interactive multi-select UI - choose which benchmarks to view
+- All benchmarks selected by default for quick viewing
+- Aggregates results from all `results.json` files in benchmark directories
+- Calculates scores on-demand from raw metrics
+- Baseline comparison with delta values
+- No database writes (read-only)
+- Automatically shown after running benchmarks (if enabled)
+
+**Example:**
+
+```text
+? Select benchmarks to view (use space to toggle, all selected by default):
+ ◼ baseline (benchmarks/baseline)
+ ◼ c15t-nextjs (benchmarks/c15t-nextjs)
+ ◼ cookieyes (benchmarks/cookieyes)
+
+● info Viewing results for: baseline, c15t-nextjs, cookieyes
+
+```
+
+### scores
+
+**Available via direct CLI only** - View calculated scores from existing benchmark results.
+
+```bash
+# Interactive: choose which app to view
+cookiebench scores
+
+# View scores for a specific app
+cookiebench scores c15t-nextjs
+
+# View scores for all apps
+cookiebench scores __all__
+```
+
+Features:
+- Reads existing `results.json` files from benchmark directories
+- Uses pre-calculated scores if available, or calculates them on-demand
+- Displays detailed score breakdowns by category
+- Shows insights and recommendations
+- Much faster than re-running full benchmarks
+
+**Note:** This command is not shown in the interactive menu. Use the `results` command instead for a comprehensive view with all metrics, or access `scores` directly via CLI for score-only output.
+
+### save
+
+**🔒 Admin Only** - Sync benchmark results to database.
+
+```bash
+# Interactive: choose which benchmarks to save
+cookiebench save
+
+# Save a specific benchmark
+cookiebench save c15t-nextjs
+
+# Save all benchmarks (can also select in interactive mode)
+cookiebench save __all__
+```
+
+**Requirements:**
+- `CONSENT_ADMIN=true` environment variable must be set
+- `API_URL` for the database endpoint
+- `DATABASE_URL` and `DATABASE_AUTH_TOKEN` for database access
+
+Features:
+- Multi-select interface for choosing which benchmarks to sync
+- Sends results to API endpoint (oRPC)
+- Persists to Turso/SQLite database
+- Confirmation prompt before syncing
+- Shows success/failure count
+
+**Note:** This command is hidden from the menu and unavailable unless you have admin access. Contact the Consent.io team for access credentials if needed.
+
+### db
+
+**🔒 Admin Only** - Manage the benchmark database.
+
+```bash
+# Push database schema
+cookiebench db push
+
+# View database status
+cookiebench db
+```
+
+## Configuration
+
+Create a `config.json` file in your project:
+
+```json
+{
+ "name": "my-app-cookieyes",
+ "iterations": 5,
+ "baseline": false,
+ "cookieBanner": {
+ "serviceName": "CookieYes",
+ "selectors": [
+ "#cookieyes-banner",
+ ".cky-consent-container"
+ ],
+ "serviceHosts": [
+ "cdn-cookieyes.com"
+ ],
+ "waitForVisibility": true,
+ "measureViewportCoverage": true,
+ "expectedLayoutShift": true
+ },
+ "techStack": {
+ "frameworks": ["react", "nextjs"],
+ "languages": ["typescript"],
+ "bundler": "webpack",
+ "bundleType": "esm",
+ "packageManager": "pnpm",
+ "typescript": true
+ },
+ "source": {
+ "license": "MIT",
+ "isOpenSource": true,
+ "github": "https://github.com/org/repo",
+ "npm": false
+ },
+ "includes": {
+ "backend": false,
+ "components": ["button", "banner"]
+ },
+ "company": {
+ "name": "Company Name",
+ "website": "https://company.com",
+ "avatar": "https://company.com/logo.png"
+ },
+ "tags": ["cookie-banner", "consent-management"]
+}
+```
+
+### Remote Benchmarking
+
+To benchmark a remote URL instead of building locally:
+
+```json
+{
+ "name": "production-app",
+ "remote": {
+ "enabled": true,
+ "url": "https://production.example.com",
+ "headers": {
+ "Authorization": "Bearer token"
+ }
+ }
+}
+```
+
+## Environment Variables
+
+```bash
+# Logging level (optional, default: info)
+LOG_LEVEL=debug # error | warn | info | debug
+# Note: Use LOG_LEVEL=debug to see detailed processing information
+
+# Admin access (required for save and db commands)
+CONSENT_ADMIN=true
+
+# Database configuration (required for save command)
+DATABASE_URL=libsql://your-turso-db.turso.io
+DATABASE_AUTH_TOKEN=your-auth-token
+
+# API endpoint for saving results (required for save command)
+API_URL=http://localhost:3000
+```
+
+### Admin Commands
+
+Some commands require admin access and are only available when `CONSENT_ADMIN=true` is set:
+
+- `save` - Sync benchmark results to database (admin only)
+- `db` - Manage database schema and migrations (admin only)
+
+**For Consent.io team members:**
+```bash
+# Enable admin mode
+export CONSENT_ADMIN=true
+
+# Now admin commands are available
+cookiebench save
+cookiebench db push
+```
+
+**For public users:**
+The `benchmark`, `results`, and `scores` commands work without admin access. Admin commands will be hidden from the menu and show an error if attempted.
+
+## Output
+
+### results.json
+
+Each benchmark creates a `results.json` file with:
+- Raw performance metrics for each iteration
+- Calculated scores (performance, bundle strategy, network impact, transparency, UX)
+- Metadata (timestamp, iterations, configuration)
+
+### Console Output
+
+The CLI displays:
+- Progress indicators for each iteration
+- Performance metrics (FCP, LCP, CLS, TTI, TBT)
+- Cookie banner detection results
+- Comprehensive scores with grades (Excellent/Good/Fair/Poor/Critical)
+- Insights and recommendations
+
+## Scoring
+
+The tool calculates scores across five categories:
+
+1. **Performance** (30%): FCP, LCP, CLS, TTI, TBT
+2. **Bundle Strategy** (25%): Bundle size, loading approach, execution time
+3. **Network Impact** (20%): Third-party requests, cookie service overhead
+4. **Transparency** (15%): Open source status, documentation, licensing
+5. **User Experience** (10%): Banner timing, coverage, layout shifts
+
+## Development
+
+```bash
+# Build the CLI
+pnpm build
+
+# Run in development
+pnpm dev
+
+# Type checking
+pnpm check-types
+
+# Linting
+pnpm lint
+```
+
+## License
+
+MIT
+
diff --git a/packages/cookiebench-cli/package.json b/packages/cookiebench-cli/package.json
new file mode 100644
index 0000000..02ce715
--- /dev/null
+++ b/packages/cookiebench-cli/package.json
@@ -0,0 +1,40 @@
+{
+ "name": "cookiebench",
+ "version": "0.0.1",
+ "private": true,
+ "type": "module",
+ "exports": "./dist/index.mjs",
+ "main": "./dist/index.mjs",
+ "module": "dist/index.mjs",
+ "bin": {
+ "cookiebench": "dist/index.mjs"
+ },
+ "scripts": {
+ "build": "rslib build",
+ "check-types": "tsc --noEmit",
+ "dev": "rslib build --watch",
+ "fmt": "biome format . --write",
+ "lint": "biome lint .",
+ "prestart": "pnpm -w turbo run build --filter=./packages/cookiebench-cli...",
+ "start": "node ./dist/index.mjs"
+ },
+ "dependencies": {
+ "@c15t/logger": "^1.0.1",
+ "@clack/prompts": "^1.0.1",
+ "@consentio/benchmark": "workspace:*",
+ "@consentio/runner": "workspace:*",
+ "@consentio/shared": "workspace:*",
+ "cli-table3": "^0.6.5",
+ "dotenv": "^17.3.1",
+ "figlet": "^1.10.0",
+ "picocolors": "^1.1.1",
+ "pretty-ms": "^9.3.0"
+ },
+ "devDependencies": {
+ "@rsdoctor/rspack-plugin": "^1.5.2",
+ "@rslib/core": "^0.16.1",
+ "@types/figlet": "^1.7.0",
+ "@types/node": "catalog:",
+ "typescript": "catalog:"
+ }
+}
diff --git a/packages/cookiebench-cli/rslib.config.ts b/packages/cookiebench-cli/rslib.config.ts
new file mode 100644
index 0000000..40e72d0
--- /dev/null
+++ b/packages/cookiebench-cli/rslib.config.ts
@@ -0,0 +1,24 @@
+import { defineConfig } from "@rslib/core";
+
+export default defineConfig({
+ source: {
+ entry: {
+ index: "./src/index.ts",
+ },
+ exclude: ["figlet"],
+ },
+ lib: [
+ {
+ bundle: true,
+ dts: false,
+ format: "esm",
+ },
+ ],
+ output: {
+ target: "node",
+ cleanDistPath: true,
+ filename: {
+ js: "[name].mjs",
+ },
+ },
+});
diff --git a/packages/cookiebench-cli/src/commands/benchmark.ts b/packages/cookiebench-cli/src/commands/benchmark.ts
new file mode 100644
index 0000000..037dea9
--- /dev/null
+++ b/packages/cookiebench-cli/src/commands/benchmark.ts
@@ -0,0 +1,593 @@
+import { mkdir, readdir, writeFile } from "node:fs/promises";
+import { join } from "node:path";
+import { setTimeout } from "node:timers/promises";
+import {
+ cancel,
+ confirm,
+ intro,
+ isCancel,
+ multiselect,
+ outro,
+ text,
+} from "@clack/prompts";
+import {
+ type BenchmarkResult,
+ BenchmarkRunner,
+ buildAndServeNextApp,
+ cleanupServer,
+ type ServerInfo,
+} from "@consentio/runner";
+import color from "picocolors";
+import {
+ DEFAULT_DOM_SIZE,
+ DEFAULT_ITERATIONS,
+ findProjectRoot,
+ HALF_SECOND,
+ PERCENTAGE_DIVISOR,
+ readConfig,
+ resolveBenchmarkPath,
+ SEPARATOR_WIDTH,
+} from "../utils";
+import type { CliLogger } from "../utils/logger";
+import { calculateScores, printScores } from "../utils/scoring";
+
+/**
+ * Calculate average from array
+ */
+function calculateAverage(values: number[]): number {
+ if (values.length === 0) {
+ return 0;
+ }
+ return values.reduce((acc, curr) => acc + curr, 0) / values.length;
+}
+
+/**
+ * Calculate timing metrics from benchmark results
+ */
+function calculateTimingMetrics(details: BenchmarkResult["details"]) {
+ return {
+ fcp: calculateAverage(details.map((d) => d.timing.firstContentfulPaint)),
+ lcp: calculateAverage(details.map((d) => d.timing.largestContentfulPaint)),
+ cls: calculateAverage(details.map((d) => d.timing.cumulativeLayoutShift)),
+ tbt: calculateAverage(
+ details.map((d) => d.timing.mainThreadBlocking.total)
+ ),
+ tti: calculateAverage(details.map((d) => d.timing.timeToInteractive)),
+ };
+}
+
+/**
+ * Calculate size metrics from benchmark results
+ */
+function calculateSizeMetrics(details: BenchmarkResult["details"]) {
+ return {
+ totalSize: calculateAverage(details.map((d) => d.size.total)),
+ jsSize: calculateAverage(details.map((d) => d.size.scripts.total)),
+ cssSize: calculateAverage(details.map((d) => d.size.styles)),
+ imageSize: calculateAverage(details.map((d) => d.size.images)),
+ fontSize: calculateAverage(details.map((d) => d.size.fonts)),
+ otherSize: calculateAverage(details.map((d) => d.size.other)),
+ };
+}
+
+/**
+ * Calculate network metrics from benchmark results
+ */
+function calculateNetworkMetrics(details: BenchmarkResult["details"]) {
+ const totalRequests = calculateAverage(
+ details.map(
+ (d) =>
+ d.resources.scripts.length +
+ d.resources.styles.length +
+ d.resources.images.length +
+ d.resources.fonts.length +
+ d.resources.other.length
+ )
+ );
+
+ const thirdPartyRequests = calculateAverage(
+ details.map(
+ (d) =>
+ d.resources.scripts.filter((r) => r.isThirdParty).length +
+ d.resources.styles.filter((r) => r.isThirdParty).length +
+ d.resources.images.filter((r) => r.isThirdParty).length +
+ d.resources.fonts.filter((r) => r.isThirdParty).length +
+ d.resources.other.filter((r) => r.isThirdParty).length
+ )
+ );
+
+ const thirdPartySize = calculateAverage(
+ details.map((d) => d.size.thirdParty)
+ );
+
+ const thirdPartyDomains = calculateAverage(
+ details.map((d) => {
+ const domains = new Set();
+ for (const resource of [
+ ...d.resources.scripts,
+ ...d.resources.styles,
+ ...d.resources.images,
+ ...d.resources.fonts,
+ ...d.resources.other,
+ ]) {
+ if (resource.isThirdParty && resource.name) {
+ try {
+ domains.add(new URL(resource.name).hostname);
+ } catch {
+ // Skip invalid URLs
+ }
+ }
+ }
+ return domains.size;
+ })
+ );
+
+ return {
+ totalRequests,
+ thirdPartyRequests,
+ thirdPartySize,
+ thirdPartyDomains,
+ };
+}
+
+/**
+ * Calculate cookie banner metrics from benchmark results.
+ *
+ * Aggregates cookie banner metrics across all iterations and computes average
+ * user-visible time (ms) for scoring. Uses bannerVisibilityTime (user-perceived
+ * visibility with opacity > 0.5) rather than bannerRenderTime (technical render
+ * time) so scores reflect actual user experience.
+ *
+ * Scoring methodology:
+ * - Requires consistent detection across ALL iterations for positive score
+ * - Uses average of visibilityTime across iterations
+ * - Applies penalties for inconsistent detection
+ * - Calculates viewport coverage percentage
+ *
+ * @param details Array of benchmark details from all iterations
+ * @param logger Logger instance for warnings
+ * @returns Aggregated cookie banner metrics for scoring
+ * @see METHODOLOGY.md for detailed explanation of render time vs visibility time
+ */
+function calculateCookieBannerMetrics(
+ details: BenchmarkResult["details"],
+ logger: CliLogger
+) {
+ // Require consistent detection across ALL iterations for true positive
+ const allDetected = details.every((r) => r.cookieBanner.detected);
+ if (!allDetected) {
+ logger.warn(
+ "⚠️ [SCORING] Banner detection inconsistent or failed - marking as not detected"
+ );
+ }
+
+ // Calculate user-visible time (ms) for scoring
+ const detectionSuccess = details.some((r) => r.cookieBanner.detected);
+ let cookieBannerVisibleTimeMs: number | null = null;
+
+ if (detectionSuccess) {
+ /**
+ * Use user-visible time (opacity-based) for scoring, not DOM presence time.
+ * Prefer userVisibleTime when present (runner output), else visibilityTime.
+ */
+ const timingValues = details.map(
+ (r) => r.cookieBanner.userVisibleTime ?? r.cookieBanner.visibilityTime
+ );
+ const hasNullValues = timingValues.some((t) => t === null || t === 0);
+
+ if (hasNullValues) {
+ logger.warn(
+ "⚠️ [SCORING] Inconsistent banner detection - applying penalty"
+ );
+ } else {
+ const validTimings = timingValues.filter(
+ (t): t is number => t !== null && t > 0
+ );
+ if (validTimings.length === details.length && validTimings.length > 0) {
+ cookieBannerVisibleTimeMs = calculateAverage(validTimings);
+ }
+ }
+ } else {
+ logger.warn(
+ "⚠️ [SCORING] No banner detected in any iteration - applying penalty"
+ );
+ }
+
+ // Calculate coverage
+ let cookieBannerCoverage = 0;
+ if (allDetected) {
+ cookieBannerCoverage =
+ calculateAverage(details.map((d) => d.cookieBanner.viewportCoverage)) /
+ PERCENTAGE_DIVISOR;
+ } else {
+ logger.warn("⚠️ [SCORING] Inconsistent detection - setting coverage to 0");
+ }
+
+ return {
+ cookieBannerDetected: allDetected,
+ cookieBannerVisibleTimeMs,
+ cookieBannerCoverage,
+ };
+}
+
+/**
+ * Calculate performance metrics from benchmark results
+ */
+function calculatePerformanceMetrics(details: BenchmarkResult["details"]) {
+ return {
+ domSize: calculateAverage(
+ details.map((d) => d.dom?.size ?? DEFAULT_DOM_SIZE)
+ ),
+ mainThreadBlocking: calculateAverage(
+ details.map((d) => d.timing.mainThreadBlocking.total)
+ ),
+ layoutShifts: calculateAverage(
+ details.map((d) => d.timing.cumulativeLayoutShift)
+ ),
+ };
+}
+
+/**
+ * Find all benchmark directories
+ */
+async function findBenchmarkDirs(
+ logger: CliLogger,
+ projectRoot: string
+): Promise {
+ const benchmarksDir = join(projectRoot, "benchmarks");
+ try {
+ const entries = await readdir(benchmarksDir, { withFileTypes: true });
+ const dirs = entries
+ .filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
+ .map((entry) => entry.name);
+ return dirs;
+ } catch (error) {
+ logger.debug("Failed to read benchmarks directory:", error);
+ return [];
+ }
+}
+
+/**
+ * Run a single benchmark for a specific app
+ */
+async function runSingleBenchmark(
+ logger: CliLogger,
+ appPath: string,
+ showScores = true,
+ iterationsOverride?: number
+): Promise {
+ const configPath = appPath ? join(appPath, "config.json") : undefined;
+ const config = readConfig(configPath);
+ if (!config) {
+ logger.error(
+ `Failed to read config.json for ${appPath || "current directory"}`
+ );
+ return false;
+ }
+
+ // Override iterations if provided
+ if (iterationsOverride !== undefined && iterationsOverride > 0) {
+ config.iterations = iterationsOverride;
+ }
+
+ try {
+ let serverInfo: ServerInfo | null = null;
+ let benchmarkUrl: string;
+
+ // Check if remote benchmarking is enabled
+ if (config.remote?.enabled && config.remote.url) {
+ logger.info(`🌐 Running remote benchmark against: ${config.remote.url}`);
+ benchmarkUrl = config.remote.url;
+ } else {
+ logger.info("🏗️ Building and serving app locally...");
+ serverInfo = await buildAndServeNextApp(logger, appPath);
+ benchmarkUrl = serverInfo.url;
+ }
+
+ const cwd = appPath || process.cwd();
+
+ // Create traces directory if it doesn't exist
+ const tracesDir = join(cwd, "traces");
+ try {
+ await mkdir(tracesDir, { recursive: true });
+ } catch (error: unknown) {
+ // Ignore EEXIST - directory already exists
+ if (
+ error &&
+ typeof error === "object" &&
+ "code" in error &&
+ error.code !== "EEXIST"
+ ) {
+ throw error;
+ }
+ }
+ logger.info(`📊 Tracing enabled - traces will be saved to: ${tracesDir}`);
+
+ try {
+ // Create benchmark runner and run benchmarks with trace saving enabled
+ const runner = new BenchmarkRunner(config, logger, {
+ saveTrace: true,
+ traceDir: tracesDir,
+ });
+ const result = await runner.runBenchmarks(benchmarkUrl);
+
+ if (!result.details || result.details.length === 0) {
+ logger.error("No successful benchmark iterations");
+ return false;
+ }
+
+ // Create app data for transparency scoring
+ const appData = {
+ name: config.name,
+ baseline: config.baseline ?? false,
+ company: config.company ? JSON.stringify(config.company) : null,
+ techStack: JSON.stringify(config.techStack),
+ source: config.source ? JSON.stringify(config.source) : null,
+ tags: config.tags ? JSON.stringify(config.tags) : null,
+ };
+
+ // Calculate all metrics using helper functions
+ const timingMetrics = calculateTimingMetrics(result.details);
+ const sizeMetrics = calculateSizeMetrics(result.details);
+ const networkMetrics = calculateNetworkMetrics(result.details);
+ const cookieBannerMetrics = calculateCookieBannerMetrics(
+ result.details,
+ logger
+ );
+ const performanceMetrics = calculatePerformanceMetrics(result.details);
+
+ // Calculate scores
+ const scores = calculateScores(
+ timingMetrics,
+ sizeMetrics,
+ networkMetrics,
+ cookieBannerMetrics,
+ performanceMetrics,
+ config.baseline ?? false,
+ appData
+ );
+
+ // Format results for results.json
+ const resultsData = {
+ app: config.name,
+ techStack: config.techStack,
+ source: config.source,
+ includes: config.includes,
+ internationalization: config.internationalization,
+ company: config.company,
+ tags: config.tags,
+ results: result.details,
+ scores,
+ metadata: {
+ timestamp: new Date().toISOString(),
+ iterations: config.iterations,
+ languages: config.techStack.languages,
+ isRemote: config.remote?.enabled ?? false,
+ url: config.remote?.enabled ? config.remote.url : undefined,
+ },
+ };
+
+ // Write results to file
+ const outputPath = join(cwd, "results.json");
+ await writeFile(outputPath, JSON.stringify(resultsData, null, 2));
+ logger.success(`Benchmark results saved to ${outputPath}`);
+
+ // Print scores if requested
+ if (showScores && scores) {
+ logger.info("📊 Benchmark Scores:");
+ printScores(scores);
+ }
+
+ return true;
+ } finally {
+ // Only cleanup server if we started one
+ if (serverInfo) {
+ cleanupServer(serverInfo);
+ }
+ }
+ } catch (error: unknown) {
+ if (error instanceof Error) {
+ logger.error(`Error running benchmark: ${error.message}`);
+ } else {
+ logger.error("An unknown error occurred during benchmark");
+ }
+ return false;
+ }
+}
+
+/**
+ * Main benchmark command with multi-select support
+ */
+export async function benchmarkCommand(
+ logger: CliLogger,
+ appPath?: string
+): Promise {
+ const projectRoot = findProjectRoot();
+
+ // If a specific app path is provided, run that benchmark directly
+ if (appPath) {
+ const resolvedAppPath = resolveBenchmarkPath(projectRoot, appPath);
+ const success = await runSingleBenchmark(logger, resolvedAppPath, true);
+ if (!success) {
+ throw new Error(`Benchmark failed for ${appPath}`);
+ }
+ return;
+ }
+
+ // Otherwise, show multi-select for available benchmarks
+ logger.clear();
+ await setTimeout(HALF_SECOND);
+
+ intro(`${color.bgMagenta(color.white(" benchmark "))}`);
+
+ // Find available benchmarks
+ const availableBenchmarks = await findBenchmarkDirs(logger, projectRoot);
+
+ if (availableBenchmarks.length === 0) {
+ logger.error("No benchmarks found in the benchmarks/ directory");
+ logger.info(
+ "Create benchmark directories with config.json files to get started"
+ );
+ throw new Error("No benchmarks found");
+ }
+
+ logger.info(
+ `Found ${availableBenchmarks.length} benchmark(s): ${color.cyan(availableBenchmarks.join(", "))}`
+ );
+
+ // Ask user to select benchmarks
+ const selectedBenchmarks = await multiselect({
+ message: "Select benchmarks to run (use space to toggle):",
+ options: availableBenchmarks.map((name) => ({
+ value: name,
+ label: name,
+ hint: join("benchmarks", name),
+ })),
+ required: true,
+ });
+
+ if (isCancel(selectedBenchmarks)) {
+ cancel("Operation cancelled");
+ return;
+ }
+
+ if (!Array.isArray(selectedBenchmarks) || selectedBenchmarks.length === 0) {
+ logger.warn("No benchmarks selected");
+ return;
+ }
+
+ // Load configs to get default iterations
+ const benchmarkConfigs = new Map();
+ for (const benchmarkName of selectedBenchmarks) {
+ const benchmarkPath = join(projectRoot, "benchmarks", benchmarkName);
+ const configPath = join(benchmarkPath, "config.json");
+ const config = readConfig(configPath);
+ if (config) {
+ benchmarkConfigs.set(benchmarkName, config.iterations);
+ }
+ }
+
+ // Find the most common iteration count or first one
+ const defaultIterations =
+ benchmarkConfigs.size > 0
+ ? Array.from(benchmarkConfigs.values())[0]
+ : DEFAULT_ITERATIONS;
+
+ // Show iteration counts for selected benchmarks
+ const iterationsList = Array.from(selectedBenchmarks)
+ .map((name) => {
+ const iterations = benchmarkConfigs.get(name) ?? "?";
+ return `${name}: ${iterations}`;
+ })
+ .join(", ");
+
+ logger.info(`Config iterations: ${color.dim(iterationsList)}`);
+
+ // Ask for iterations override
+ const iterationsInput = await text({
+ message: "Number of iterations (press Enter to use config values):",
+ placeholder: `Default: ${defaultIterations}`,
+ defaultValue: "",
+ validate: (value) => {
+ if (!value || value === "") {
+ return; // Empty is valid (use defaults)
+ }
+ const num = Number.parseInt(value, 10);
+ if (Number.isNaN(num) || num < 1) {
+ return "Please enter a valid number greater than 0";
+ }
+ },
+ });
+
+ if (isCancel(iterationsInput)) {
+ cancel("Operation cancelled");
+ return;
+ }
+
+ // Parse iterations - if empty string, use undefined to let each benchmark use its config
+ const iterationsOverride =
+ iterationsInput === "" ? undefined : Number.parseInt(iterationsInput, 10);
+
+ if (iterationsOverride !== undefined) {
+ logger.info(
+ `Using ${color.bold(color.cyan(String(iterationsOverride)))} iterations for all benchmarks`
+ );
+ } else {
+ logger.info("Using iteration counts from each benchmark config");
+ }
+
+ // Ask if user wants to see results panel after completion
+ const showResults = await confirm({
+ message: "Show results panel after completion?",
+ initialValue: true,
+ });
+
+ if (isCancel(showResults)) {
+ cancel("Operation cancelled");
+ return;
+ }
+
+ // Run selected benchmarks sequentially
+ const results: Array<{ name: string; success: boolean }> = [];
+
+ for (let i = 0; i < selectedBenchmarks.length; i += 1) {
+ const benchmarkName = selectedBenchmarks[i];
+ const benchmarkPath = join(projectRoot, "benchmarks", benchmarkName);
+
+ logger.info(
+ `\n${color.bold(color.cyan(`[${i + 1}/${selectedBenchmarks.length}]`))} Running benchmark: ${color.bold(benchmarkName)}`
+ );
+
+ const success = await runSingleBenchmark(
+ logger,
+ benchmarkPath,
+ false, // Don't show inline scores anymore
+ iterationsOverride
+ );
+
+ results.push({ name: benchmarkName, success });
+
+ if (!success) {
+ logger.error(
+ `Failed to complete benchmark for ${benchmarkName}, continuing...`
+ );
+ }
+
+ // Add spacing between benchmarks
+ if (i < selectedBenchmarks.length - 1) {
+ logger.message(`\n${"─".repeat(SEPARATOR_WIDTH)}\n`);
+ }
+ }
+
+ // Summary
+ logger.message("\n");
+ outro(
+ `${color.bold("Summary:")} ${results.filter((r) => r.success).length}/${results.length} benchmarks completed successfully`
+ );
+
+ // Show failed benchmarks if any
+ const failed = results.filter((r) => !r.success);
+ if (failed.length > 0) {
+ logger.warn(`Failed benchmarks: ${failed.map((r) => r.name).join(", ")}`);
+ }
+
+ // Show results panel if requested
+ if (showResults === true && results.some((r) => r.success)) {
+ logger.message(`\n${"═".repeat(SEPARATOR_WIDTH)}\n`);
+ logger.info("Loading results panel...\n");
+
+ // Get successful benchmark names
+ const successfulBenchmarks = results
+ .filter((r) => r.success)
+ .map((r) => r.name);
+
+ // Dynamically import and run the results command with specific benchmarks
+ try {
+ const { resultsCommand } = await import("./results.js");
+ await resultsCommand(logger, successfulBenchmarks);
+ } catch (error) {
+ logger.error("Failed to load results panel");
+ logger.debug(error instanceof Error ? error.message : String(error));
+ }
+ }
+}
diff --git a/packages/cookiebench-cli/src/commands/db.ts b/packages/cookiebench-cli/src/commands/db.ts
new file mode 100644
index 0000000..d576406
--- /dev/null
+++ b/packages/cookiebench-cli/src/commands/db.ts
@@ -0,0 +1,281 @@
+import { execFileSync } from "node:child_process";
+import { existsSync } from "node:fs";
+import { dirname, join } from "node:path";
+import { setTimeout } from "node:timers/promises";
+import { cancel, confirm, intro, isCancel, select } from "@clack/prompts";
+import { ONE_SECOND } from "@consentio/shared";
+import color from "picocolors";
+import { isAdminUser } from "../utils/auth";
+import type { CliLogger } from "../utils/logger";
+
+const DB_PACKAGE_RELATIVE_PATH = join("packages", "db");
+
+function ensureDbPackage(logger: CliLogger, projectRoot: string) {
+ const dbPackagePath = join(projectRoot, DB_PACKAGE_RELATIVE_PATH);
+ const drizzleConfigPath = join(dbPackagePath, "drizzle.config.ts");
+
+ if (!existsSync(dbPackagePath)) {
+ logger.error(
+ "Database package not found. Make sure you are running this from the project root."
+ );
+ process.exit(1);
+ }
+
+ if (!existsSync(drizzleConfigPath)) {
+ logger.error(
+ "Drizzle config not found. Make sure drizzle.config.ts exists in packages/db/"
+ );
+ process.exit(1);
+ }
+}
+
+function runDrizzleCommand(
+ logger: CliLogger,
+ projectRoot: string,
+ command: string
+): void {
+ const dbPackagePath = join(projectRoot, DB_PACKAGE_RELATIVE_PATH);
+
+ try {
+ logger.step(`Running: ${color.cyan(`drizzle-kit ${command}`)}`);
+ execFileSync("pnpm", ["drizzle-kit", command], {
+ cwd: dbPackagePath,
+ stdio: "inherit",
+ });
+ } catch (error) {
+ logger.error(`Failed to run drizzle-kit ${command}`);
+ if (error instanceof Error) {
+ logger.error(error.message);
+ }
+ process.exit(1);
+ }
+}
+
+export async function dbCommand(logger: CliLogger, subcommand?: string) {
+ // Double-check admin access (safeguard)
+ if (!isAdminUser()) {
+ logger.error("This command requires admin access");
+ process.exit(1);
+ }
+
+ logger.clear();
+ await setTimeout(ONE_SECOND);
+
+ intro(`${color.bgBlue(color.white(" database "))} ${color.dim("v0.1.0")}`);
+
+ const projectRoot = findProjectRoot();
+ ensureDbPackage(logger, projectRoot);
+
+ let selectedCommand = subcommand;
+
+ if (!selectedCommand) {
+ const command = await select({
+ message: "What would you like to do?",
+ options: [
+ {
+ value: "push",
+ label: "Push schema changes",
+ hint: "Push schema directly to database (good for development)",
+ },
+ {
+ value: "generate",
+ label: "Generate migrations",
+ hint: "Generate SQL migration files from schema changes",
+ },
+ {
+ value: "migrate",
+ label: "Run migrations",
+ hint: "Apply migration files to the database",
+ },
+ {
+ value: "studio",
+ label: "Open database studio",
+ hint: "Browse and edit your database with Drizzle Studio",
+ },
+ {
+ value: "status",
+ label: "Check migration status",
+ hint: "See which migrations have been applied",
+ },
+ ],
+ });
+
+ if (isCancel(command)) {
+ cancel("Operation cancelled.");
+ return;
+ }
+
+ selectedCommand = command;
+ }
+
+ switch (selectedCommand) {
+ case "push":
+ await pushCommand(logger, projectRoot);
+ break;
+ case "generate":
+ await generateCommand(logger, projectRoot);
+ break;
+ case "migrate":
+ await migrateCommand(logger, projectRoot);
+ break;
+ case "studio":
+ await studioCommand(logger, projectRoot);
+ break;
+ case "status":
+ await statusCommand(logger, projectRoot);
+ break;
+ default:
+ logger.error(`Unknown subcommand: ${selectedCommand}`);
+ logger.info(
+ "Available commands: push, generate, migrate, studio, status"
+ );
+ process.exit(1);
+ }
+}
+
+async function pushCommand(logger: CliLogger, projectRoot: string) {
+ logger.step("Pushing schema changes to database...");
+ logger.info("This will apply schema changes directly to your database.");
+ logger.warn("This is recommended for development only!");
+
+ const confirmCommand = await confirm({
+ message: "Are you sure you want to push schema changes?",
+ initialValue: false,
+ });
+
+ if (isCancel(confirmCommand) || !confirmCommand) {
+ cancel("Push cancelled.");
+ return;
+ }
+
+ runDrizzleCommand(logger, projectRoot, "push");
+ logger.success("Schema pushed successfully!");
+ logger.outro("Database is now up to date with your schema.");
+}
+
+function generateCommand(logger: CliLogger, projectRoot: string) {
+ logger.step("Generating migration files...");
+ logger.info("This will create SQL migration files based on schema changes.");
+
+ runDrizzleCommand(logger, projectRoot, "generate");
+ logger.success("Migration files generated!");
+ logger.info(
+ "Review the generated files in packages/db/drizzle/ before applying them."
+ );
+ logger.outro(`Run ${color.cyan("cli db migrate")} to apply the migrations.`);
+}
+
+async function migrateCommand(logger: CliLogger, projectRoot: string) {
+ logger.step("Running migrations...");
+ logger.info("This will apply pending migration files to your database.");
+
+ const confirmCommand = await confirm({
+ message: "Are you sure you want to run migrations?",
+ initialValue: true,
+ });
+
+ if (isCancel(confirmCommand) || !confirmCommand) {
+ cancel("Migration cancelled.");
+ return;
+ }
+
+ try {
+ runDrizzleCommand(logger, projectRoot, "migrate");
+ logger.success("Migrations completed successfully!");
+ logger.outro("Database is now up to date.");
+ } catch (error) {
+ logger.error("Migration failed!");
+ if (error instanceof Error) {
+ logger.error(error.message);
+ }
+ process.exit(1);
+ }
+}
+
+function studioCommand(logger: CliLogger, projectRoot: string) {
+ logger.step("Opening Drizzle Studio...");
+ logger.info(
+ "This will start a web interface to browse and edit your database."
+ );
+ logger.info("Press Ctrl+C to stop the studio when you're done.");
+
+ try {
+ runDrizzleCommand(logger, projectRoot, "studio");
+ } catch {
+ // Studio command might be interrupted by Ctrl+C, which is normal
+ logger.info("Studio closed.");
+ }
+}
+
+// Same project root finding logic as in db package
+function findProjectRoot(): string {
+ let currentDir = process.cwd();
+
+ while (currentDir !== dirname(currentDir)) {
+ if (
+ (existsSync(join(currentDir, "pnpm-workspace.yaml")) ||
+ existsSync(join(currentDir, "package.json"))) &&
+ existsSync(join(currentDir, "packages"))
+ ) {
+ return currentDir;
+ }
+ currentDir = dirname(currentDir);
+ }
+
+ return process.cwd();
+}
+
+async function statusCommand(logger: CliLogger, projectRoot: string) {
+ logger.step("Checking migration status...");
+
+ try {
+ const dbPath = join(projectRoot, "benchmarks.db");
+ if (!existsSync(dbPath)) {
+ logger.warn("Database file does not exist yet.");
+ logger.info(
+ `Run ${color.cyan("cli db push")} or ${color.cyan("cli db migrate")} to create it.`
+ );
+ return;
+ }
+
+ // Check migrations folder
+ const migrationsPath = join(
+ projectRoot,
+ DB_PACKAGE_RELATIVE_PATH,
+ "drizzle"
+ );
+ if (!existsSync(migrationsPath)) {
+ logger.warn("No migrations found.");
+ logger.info(
+ `Run ${color.cyan("cli db generate")} to create migration files.`
+ );
+ return;
+ }
+
+ // List migration files
+ const { readdir } = await import("node:fs/promises");
+ const migrationFiles = await readdir(migrationsPath, {
+ withFileTypes: true,
+ });
+ const migrations = migrationFiles
+ .filter((dirent) => dirent.isDirectory())
+ .map((dirent) => dirent.name)
+ .sort();
+
+ if (migrations.length === 0) {
+ logger.info("No migration files found.");
+ } else {
+ logger.info(`Found ${migrations.length} migration(s):`);
+ for (const migration of migrations) {
+ logger.info(` - ${migration}`);
+ }
+ }
+
+ logger.success("Status check complete.");
+ } catch (error) {
+ logger.error("Failed to check status.");
+ if (error instanceof Error) {
+ logger.error(error.message);
+ }
+ }
+}
diff --git a/packages/cookiebench-cli/src/commands/results.ts b/packages/cookiebench-cli/src/commands/results.ts
new file mode 100644
index 0000000..da203a2
--- /dev/null
+++ b/packages/cookiebench-cli/src/commands/results.ts
@@ -0,0 +1,1176 @@
+/** biome-ignore-all lint/suspicious/noConsole: console output needed for results display */
+
+import { readdir, readFile } from "node:fs/promises";
+import { join } from "node:path";
+import { setTimeout } from "node:timers/promises";
+
+import { cancel, intro, isCancel, multiselect } from "@clack/prompts";
+import type { Config } from "@consentio/runner";
+import { KILOBYTE, ONE_SECOND, PERCENTAGE_DIVISOR } from "@consentio/shared";
+import Table from "cli-table3";
+import color from "picocolors";
+import prettyMilliseconds from "pretty-ms";
+import type { BenchmarkScores } from "../types";
+import { isAdminUser } from "../utils/auth";
+import {
+ CLS_DECIMAL_PLACES,
+ CLS_THRESHOLD_FAIR,
+ CLS_THRESHOLD_GOOD,
+ COL_WIDTH_CHART_PADDING,
+ COL_WIDTH_DURATION,
+ COL_WIDTH_NAME,
+ COL_WIDTH_SIZE,
+ COL_WIDTH_SOURCE,
+ COL_WIDTH_TAGS,
+ COL_WIDTH_TYPE,
+ DEFAULT_DOM_SIZE,
+ MAX_FILENAME_LENGTH,
+ MIN_DURATION_THRESHOLD,
+ SCORE_THRESHOLD_FAIR,
+ SCORE_THRESHOLD_POOR,
+ TRUNCATED_FILENAME_LENGTH,
+} from "../utils/constants";
+import { findProjectRoot } from "../utils/project-root";
+import type { CliLogger } from "../utils/logger";
+import { calculateScores } from "../utils/scoring";
+
+// Raw benchmark data structure from JSON files
+export type RawBenchmarkDetail = {
+ duration: number;
+ size: {
+ total: number;
+ bundled: number;
+ thirdParty: number;
+ cookieServices: number;
+ scripts: {
+ total: number;
+ initial: number;
+ dynamic: number;
+ thirdParty: number;
+ cookieServices: number;
+ };
+ styles: number;
+ images: number;
+ fonts: number;
+ other: number;
+ };
+ timing: {
+ navigationStart: number;
+ domContentLoaded: number;
+ load: number;
+ firstPaint: number;
+ firstContentfulPaint: number;
+ largestContentfulPaint: number;
+ timeToInteractive: number;
+ cumulativeLayoutShift: number;
+ // NEW: Perfume.js enhanced metrics
+ timeToFirstByte?: number;
+ firstInputDelay?: number | null;
+ interactionToNextPaint?: number | null;
+ navigationTiming?: {
+ timeToFirstByte: number;
+ domInteractive: number;
+ domContentLoadedEventStart: number;
+ domContentLoadedEventEnd: number;
+ domComplete: number;
+ loadEventStart: number;
+ loadEventEnd: number;
+ };
+ networkInformation?: {
+ effectiveType: string;
+ downlink: number;
+ rtt: number;
+ saveData: boolean;
+ };
+ cookieBanner: {
+ renderStart: number;
+ renderEnd: number;
+ interactionStart: number;
+ interactionEnd: number;
+ layoutShift: number;
+ detected: boolean;
+ selector: string | null;
+ serviceName: string;
+ visibilityTime: number;
+ /** DOM presence time (ms). Present when reading runner output. */
+ domPresenceTime?: number;
+ /** User-visible time (ms). Present when reading runner output; used for scoring. */
+ userVisibleTime?: number;
+ viewportCoverage: number;
+ };
+ thirdParty: {
+ dnsLookupTime: number;
+ connectionTime: number;
+ downloadTime: number;
+ totalImpact: number;
+ cookieServices: {
+ hosts: string[];
+ totalSize: number;
+ resourceCount: number;
+ dnsLookupTime: number;
+ connectionTime: number;
+ downloadTime: number;
+ };
+ };
+ mainThreadBlocking: {
+ total: number;
+ cookieBannerEstimate: number;
+ percentageFromCookies: number;
+ };
+ scripts: {
+ bundled: {
+ loadStart: number;
+ loadEnd: number;
+ executeStart: number;
+ executeEnd: number;
+ };
+ thirdParty: {
+ loadStart: number;
+ loadEnd: number;
+ executeStart: number;
+ executeEnd: number;
+ };
+ };
+ };
+ resources: {
+ scripts: Array<{
+ name: string;
+ size: number;
+ duration: number;
+ startTime: number;
+ isThirdParty: boolean;
+ isDynamic: boolean;
+ isCookieService: boolean;
+ dnsTime?: number;
+ connectionTime?: number;
+ }>;
+ styles: Array<{
+ name: string;
+ size: number;
+ duration: number;
+ startTime: number;
+ isThirdParty: boolean;
+ isCookieService: boolean;
+ }>;
+ images: Array<{
+ name: string;
+ size: number;
+ duration: number;
+ startTime: number;
+ isThirdParty: boolean;
+ isCookieService: boolean;
+ }>;
+ fonts: Array<{
+ name: string;
+ size: number;
+ duration: number;
+ startTime: number;
+ isThirdParty: boolean;
+ isCookieService: boolean;
+ }>;
+ other: Array<{
+ name: string;
+ size: number;
+ duration: number;
+ startTime: number;
+ isThirdParty: boolean;
+ isCookieService: boolean;
+ type: string;
+ }>;
+ };
+ language: string;
+};
+
+export type BenchmarkOutput = {
+ app: string;
+ results: RawBenchmarkDetail[];
+ scores?: {
+ totalScore: number;
+ grade: "Excellent" | "Good" | "Fair" | "Poor" | "Critical";
+ categoryScores: {
+ performance: number;
+ bundleStrategy: number;
+ networkImpact: number;
+ transparency: number;
+ userExperience: number;
+ };
+ categories: Array<{
+ name: string;
+ score: number;
+ maxScore: number;
+ weight: number;
+ details: Array<{
+ name: string;
+ score: number;
+ maxScore: number;
+ reason: string;
+ }>;
+ status: "good" | "warning" | "critical";
+ }>;
+ insights: string[];
+ recommendations: string[];
+ };
+ metadata: {
+ timestamp: string;
+ iterations: number;
+ language: string;
+ };
+};
+
+async function findResultsFiles(dir: string): Promise {
+ const files: string[] = [];
+ try {
+ const entries = await readdir(dir, { withFileTypes: true });
+ for (const entry of entries) {
+ const fullPath = join(dir, entry.name);
+ if (entry.isDirectory()) {
+ files.push(...(await findResultsFiles(fullPath)));
+ } else if (entry.name === "results.json") {
+ files.push(fullPath);
+ }
+ }
+ } catch (error) {
+ const errorCode =
+ typeof error === "object" &&
+ error !== null &&
+ "code" in error &&
+ typeof error.code === "string"
+ ? error.code
+ : "";
+ if (errorCode === "ENOENT") {
+ return files;
+ }
+ throw error;
+ }
+
+ return files;
+}
+
+async function loadConfigForApp(
+ logger: CliLogger,
+ appName: string,
+ projectRoot: string
+): Promise {
+ const configPath = join(projectRoot, "benchmarks", appName, "config.json");
+
+ try {
+ const configContent = await readFile(configPath, "utf-8");
+ const config = JSON.parse(configContent);
+
+ return {
+ name: config.name || appName,
+ iterations: config.iterations || 0,
+ techStack: config.techStack || {
+ languages: [],
+ frameworks: [],
+ bundler: "unknown",
+ bundleType: "unknown",
+ packageManager: "unknown",
+ typescript: false,
+ },
+ source: config.source || {
+ license: "unknown",
+ isOpenSource: false,
+ github: false,
+ npm: false,
+ },
+ includes: config.includes || { backend: [], components: [] },
+ company: config.company || undefined,
+ tags: config.tags || [],
+ cookieBanner: config.cookieBanner || {
+ serviceName: "Unknown",
+ selectors: [],
+ serviceHosts: [],
+ waitForVisibility: false,
+ measureViewportCoverage: false,
+ expectedLayoutShift: false,
+ },
+ internationalization: config.internationalization || {
+ detection: "none",
+ stringLoading: "bundled",
+ },
+ };
+ } catch (error) {
+ logger.debug(
+ `Could not load config for ${appName}: ${
+ error instanceof Error ? error.message : "Unknown error"
+ }`
+ );
+ return {
+ name: appName,
+ iterations: 0,
+ techStack: {
+ languages: [],
+ frameworks: [],
+ bundler: "unknown",
+ bundleType: "unknown",
+ packageManager: "unknown",
+ typescript: false,
+ },
+ source: {
+ license: "unknown",
+ isOpenSource: false,
+ github: false,
+ npm: false,
+ },
+ includes: {
+ backend: [],
+ components: [],
+ },
+ company: undefined,
+ tags: [],
+ cookieBanner: {
+ serviceName: "Unknown",
+ selectors: [],
+ serviceHosts: [],
+ waitForVisibility: false,
+ measureViewportCoverage: false,
+ expectedLayoutShift: false,
+ },
+ internationalization: {
+ detection: "none",
+ stringLoading: "bundled",
+ },
+ };
+ }
+}
+
+async function aggregateResults(logger: CliLogger, resultsDir: string) {
+ const resultsFiles = await findResultsFiles(resultsDir);
+ const results: Record = {};
+
+ logger.debug(`Found ${resultsFiles.length} results files:`);
+ for (const file of resultsFiles) {
+ logger.debug(` - ${file}`);
+ }
+
+ for (const file of resultsFiles) {
+ try {
+ const content = await readFile(file, "utf-8");
+ const data: BenchmarkOutput = JSON.parse(content);
+
+ if (!(data.app && data.results)) {
+ logger.warn(
+ `Skipping invalid results file: ${file} (missing app or results)`
+ );
+ continue;
+ }
+
+ logger.debug(`Processing ${file} with app name: "${data.app}"`);
+
+ if (results[data.app]) {
+ logger.warn(
+ `Duplicate app name "${data.app}" found in ${file}. Previous results will be overwritten.`
+ );
+ }
+
+ results[data.app] = data.results;
+ logger.debug(
+ `Loaded results for ${data.app} (${data.results.length} iterations)`
+ );
+ } catch (error) {
+ logger.error(
+ `Failed to process ${file}: ${
+ error instanceof Error ? error.message : "Unknown error"
+ }`
+ );
+ if (error instanceof Error && error.stack) {
+ logger.debug(`Stack trace: ${error.stack}`);
+ }
+ }
+ }
+
+ logger.debug("Final results summary:");
+ for (const [app, appResults] of Object.entries(results)) {
+ logger.debug(` - ${app}: ${appResults.length} iterations`);
+ }
+
+ return results;
+}
+
+// Minimum threshold for showing decimal milliseconds (JavaScript precision is ~0.1ms)
+const SUB_MILLISECOND_THRESHOLD = 1;
+const MILLISECOND_DECIMAL_PLACES = 3;
+
+function formatTime(ms: number): string {
+ // JavaScript timing precision is typically ~0.1ms, so we don't show microseconds
+ // For very small values, show fractional milliseconds
+ return prettyMilliseconds(ms, {
+ secondsDecimalDigits: 2,
+ keepDecimalsOnWholeSeconds: true,
+ compact: true,
+ millisecondsDecimalDigits:
+ ms < SUB_MILLISECOND_THRESHOLD ? MILLISECOND_DECIMAL_PLACES : 0,
+ });
+}
+
+function formatBytes(bytes: number): string {
+ if (bytes === 0) {
+ return "0bytes";
+ }
+ if (bytes < KILOBYTE) {
+ return `${bytes.toFixed(0)}bytes`;
+ }
+ return `${(bytes / KILOBYTE).toFixed(0)}KB`;
+}
+
+function getPerformanceRating(metric: string, value: number): string {
+ const ratings: Record = {
+ fcp: { good: 1800, poor: 3000 },
+ lcp: { good: 2500, poor: 4000 },
+ cls: { good: 0.1, poor: 0.25 },
+ tti: { good: 3800, poor: 7300 },
+ tbt: { good: 200, poor: 600 },
+ };
+
+ const thresholds = ratings[metric];
+ if (!thresholds) {
+ return "N/A";
+ }
+
+ if (value <= thresholds.good) {
+ return color.green("Good");
+ }
+ if (value <= thresholds.poor) {
+ return color.yellow("Fair");
+ }
+ return color.red("Poor");
+}
+
+function printDetailedResults(
+ appName: string,
+ results: RawBenchmarkDetail[],
+ scores: BenchmarkScores,
+ baseline?: RawBenchmarkDetail[]
+) {
+ console.log(
+ `\n${color.bold(color.cyan(`━━━ ${appName.toUpperCase()} ━━━`))}`
+ );
+
+ // ━━━ Score Display ━━━
+ const score = Math.round(scores.totalScore);
+ let scoreColor = color.green;
+ let scoreBgColor = color.bgGreen;
+
+ if (score < SCORE_THRESHOLD_POOR) {
+ scoreColor = color.red;
+ scoreBgColor = color.bgRed;
+ } else if (score < SCORE_THRESHOLD_FAIR) {
+ scoreColor = color.yellow;
+ scoreBgColor = color.bgYellow;
+ }
+
+ console.log(`\n${color.bold("🎯 Overall Score")}`);
+ console.log(
+ scoreColor(` ${score}/100`) +
+ " " +
+ scoreBgColor(color.black(` ${scores.grade} `))
+ );
+
+ // ━━━ Key Insights ━━━
+ if (scores.insights && scores.insights.length > 0) {
+ console.log(`\n${color.bold("💡 Key Insights")}`);
+ for (const insight of scores.insights) {
+ console.log(`${color.blue(" •")} ${color.dim(insight)}`);
+ }
+ }
+
+ // Calculate averages (support both timing modes; scoring uses user-visible)
+ const getUserVisibleTimeMs = (r: RawBenchmarkDetail) =>
+ r.timing.cookieBanner.userVisibleTime ??
+ r.timing.cookieBanner.visibilityTime;
+ const getDomPresenceTimeMs = (r: RawBenchmarkDetail) =>
+ r.timing.cookieBanner.domPresenceTime ?? r.timing.cookieBanner.renderStart;
+ const avgBannerDomPresenceTimeMs =
+ results.reduce((a, b) => a + getDomPresenceTimeMs(b), 0) / results.length;
+ const avgBannerVisibleTimeMs =
+ results.reduce((a, b) => a + getUserVisibleTimeMs(b), 0) / results.length;
+ const avgViewportCoverage =
+ results.reduce((a, b) => a + b.timing.cookieBanner.viewportCoverage, 0) /
+ results.length;
+ const avgNetworkImpact =
+ results.reduce((a, b) => a + b.size.thirdParty, 0) / results.length;
+ const _bannerDetected = results.some((r) => r.timing.cookieBanner.detected);
+ const isBundled = results[0]?.size.thirdParty === 0;
+
+ const avgFCP =
+ results.reduce((a, b) => a + b.timing.firstContentfulPaint, 0) /
+ results.length;
+ const avgLCP =
+ results.reduce((a, b) => a + b.timing.largestContentfulPaint, 0) /
+ results.length;
+ const avgTTI =
+ results.reduce((a, b) => a + b.timing.timeToInteractive, 0) /
+ results.length;
+ const avgCLS =
+ results.reduce((a, b) => a + b.timing.cumulativeLayoutShift, 0) /
+ results.length;
+ const avgTBT =
+ results.reduce((a, b) => a + b.timing.mainThreadBlocking.total, 0) /
+ results.length;
+
+ const totalSize =
+ results.reduce((a, b) => a + b.size.total, 0) / results.length;
+ const jsSize =
+ results.reduce((a, b) => a + b.size.scripts.total, 0) / results.length;
+ const cssSize =
+ results.reduce((a, b) => a + b.size.styles, 0) / results.length;
+ const imageSize =
+ results.reduce((a, b) => a + b.size.images, 0) / results.length;
+ const fontSize =
+ results.reduce((a, b) => a + b.size.fonts, 0) / results.length;
+ const otherSize =
+ results.reduce((a, b) => a + b.size.other, 0) / results.length;
+
+ const jsFiles =
+ results.reduce((a, b) => a + b.resources.scripts.length, 0) /
+ results.length;
+ const cssFiles =
+ results.reduce((a, b) => a + b.resources.styles.length, 0) / results.length;
+ const imageFiles =
+ results.reduce((a, b) => a + b.resources.images.length, 0) / results.length;
+ const fontFiles =
+ results.reduce((a, b) => a + b.resources.fonts.length, 0) / results.length;
+ const otherFiles =
+ results.reduce((a, b) => a + b.resources.other.length, 0) / results.length;
+
+ // Calculate deltas if baseline exists (user-visible is used for scoring)
+ let bannerDelta = "";
+ if (baseline && appName !== "baseline") {
+ const baselineAvgBanner =
+ baseline.reduce((a, b) => a + getUserVisibleTimeMs(b), 0) /
+ baseline.length;
+ const delta = avgBannerVisibleTimeMs - baselineAvgBanner;
+ bannerDelta = ` ${delta > 0 ? "+" : ""}${formatTime(delta)}`;
+ }
+
+ // ━━━ Cookie Banner Impact (dual timing modes) ━━━
+ console.log(`\n${color.bold("🍪 Cookie Banner Impact")}`);
+ console.log(
+ color.dim(" Dual timing: DOM presence (technical) | Banner visible (used for score)")
+ );
+ const bannerTable = new Table({
+ chars: { mid: "", "left-mid": "", "mid-mid": "", "right-mid": "" },
+ style: { "padding-left": 2, "padding-right": 2, border: ["grey"] },
+ });
+
+ bannerTable.push(
+ [
+ { content: "DOM presence", colSpan: 1 },
+ { content: "Banner visible (scored)", colSpan: 1 },
+ { content: "Viewport Coverage", colSpan: 1 },
+ { content: "Network Impact", colSpan: 1 },
+ { content: "Bundle Strategy", colSpan: 1 },
+ ],
+ [
+ `${color.bold(formatTime(avgBannerDomPresenceTimeMs))}\n${color.dim("Technical render")}`,
+ `${color.bold(formatTime(avgBannerVisibleTimeMs))}\n${color.dim(bannerDelta || "baseline")}`,
+ `${color.bold(`${avgViewportCoverage.toFixed(1)}%`)}\n${color.dim("Screen real estate")}`,
+ `${color.bold(formatBytes(avgNetworkImpact * KILOBYTE))}\n${color.dim(isBundled ? "Bundled (no network)" : "External requests")}`,
+ `${color.bold(isBundled ? "Bundled" : "External")}\n${color.dim(isBundled ? "Included in main bundle" : "Loaded from CDN")}`,
+ ]
+ );
+
+ console.log(bannerTable.toString());
+
+ // ━━━ Core Web Vitals ━━━
+ console.log(`\n${color.bold("⚡ Core Web Vitals")}`);
+ const vitalsTable = new Table({
+ chars: { mid: "", "left-mid": "", "mid-mid": "", "right-mid": "" },
+ style: { "padding-left": 2, "padding-right": 2, border: ["grey"] },
+ });
+
+ vitalsTable.push(
+ [
+ { content: "First Contentful Paint", colSpan: 1 },
+ { content: "Largest Contentful Paint", colSpan: 1 },
+ { content: "Time to Interactive", colSpan: 1 },
+ { content: "Cumulative Layout Shift", colSpan: 1 },
+ ],
+ [
+ `${color.bold(formatTime(avgFCP))}\n${getPerformanceRating("fcp", avgFCP)}`,
+ `${color.bold(formatTime(avgLCP))}\n${getPerformanceRating("lcp", avgLCP)}`,
+ `${color.bold(formatTime(avgTTI))}\n${getPerformanceRating("tti", avgTTI)}`,
+ `${color.bold(avgCLS.toFixed(CLS_DECIMAL_PLACES))}\n${getPerformanceRating("cls", avgCLS)}`,
+ ]
+ );
+
+ console.log(vitalsTable.toString());
+
+ // ━━━ Resource Breakdown ━━━
+ console.log(`\n${color.bold("📦 Resource Breakdown")}`);
+
+ const totalFiles = jsFiles + cssFiles + imageFiles + fontFiles + otherFiles;
+ const jsPercentage =
+ totalSize > 0 ? (jsSize / totalSize) * PERCENTAGE_DIVISOR : 0;
+ const cssPercentage =
+ totalSize > 0 ? (cssSize / totalSize) * PERCENTAGE_DIVISOR : 0;
+ const imagePercentage =
+ totalSize > 0 ? (imageSize / totalSize) * PERCENTAGE_DIVISOR : 0;
+ const fontPercentage =
+ totalSize > 0 ? (fontSize / totalSize) * PERCENTAGE_DIVISOR : 0;
+ const otherPercentage =
+ totalSize > 0 ? (otherSize / totalSize) * PERCENTAGE_DIVISOR : 0;
+
+ const resourceTable = new Table({
+ chars: { mid: "", "left-mid": "", "mid-mid": "", "right-mid": "" },
+ style: { "padding-left": 2, "padding-right": 2, border: ["grey"] },
+ });
+
+ resourceTable.push(
+ [
+ { content: "Type", colSpan: 1 },
+ { content: "Size", colSpan: 1 },
+ { content: "Files", colSpan: 1 },
+ { content: "% of Total", colSpan: 1 },
+ ],
+ [
+ color.cyan("JavaScript"),
+ formatBytes(jsSize * KILOBYTE),
+ Math.round(jsFiles).toString(),
+ `${jsPercentage.toFixed(1)}%`,
+ ],
+ [
+ color.cyan("CSS"),
+ formatBytes(cssSize * KILOBYTE),
+ Math.round(cssFiles).toString(),
+ `${cssPercentage.toFixed(1)}%`,
+ ],
+ [
+ color.cyan("Images"),
+ formatBytes(imageSize * KILOBYTE),
+ Math.round(imageFiles).toString(),
+ `${imagePercentage.toFixed(1)}%`,
+ ],
+ [
+ color.cyan("Fonts"),
+ formatBytes(fontSize * KILOBYTE),
+ Math.round(fontFiles).toString(),
+ `${fontPercentage.toFixed(1)}%`,
+ ],
+ [
+ color.cyan("Other"),
+ formatBytes(otherSize * KILOBYTE),
+ Math.round(otherFiles).toString(),
+ `${otherPercentage.toFixed(1)}%`,
+ ],
+ [
+ color.bold("Total"),
+ color.bold(formatBytes(totalSize * KILOBYTE)),
+ color.bold(Math.round(totalFiles).toString()),
+ color.bold("100%"),
+ ]
+ );
+
+ console.log(resourceTable.toString());
+
+ // ━━━ Performance Impact Summary ━━━
+ console.log(`\n${color.bold("📊 Performance Impact Summary")}`);
+ const summaryTable = new Table({
+ chars: { mid: "", "left-mid": "", "mid-mid": "", "right-mid": "" },
+ style: { "padding-left": 2, "padding-right": 2, border: ["grey"] },
+ });
+
+ let layoutStability = "Poor";
+ if (avgCLS === 0) {
+ layoutStability = "Perfect";
+ } else if (avgCLS < CLS_THRESHOLD_GOOD) {
+ layoutStability = "Good";
+ } else if (avgCLS < CLS_THRESHOLD_FAIR) {
+ layoutStability = "Fair";
+ }
+
+ summaryTable.push(
+ ["Loading Strategy", color.bold(isBundled ? "Bundled" : "External")],
+ ["Render Performance", color.bold(formatTime(avgBannerVisibleTimeMs))],
+ ["Network Overhead", color.bold(formatBytes(avgNetworkImpact * KILOBYTE))],
+ ["Main Thread Impact", color.bold(formatTime(avgTBT))],
+ ["Layout Stability", color.bold(layoutStability)],
+ ["User Disruption", color.bold(`${avgViewportCoverage.toFixed(1)}%`)]
+ );
+
+ console.log(summaryTable.toString());
+
+ // ━━━ Network Chart (Waterfall) ━━━
+ console.log(`\n${color.bold("🌐 Network Chart")}`);
+
+ // Get first iteration's resources for waterfall
+ const firstResult = results[0];
+ if (firstResult?.resources) {
+ const allResources = [
+ ...firstResult.resources.scripts.map((r) => ({ ...r, type: "script" })),
+ ...firstResult.resources.styles.map((r) => ({ ...r, type: "style" })),
+ ...firstResult.resources.images.map((r) => ({ ...r, type: "image" })),
+ ...firstResult.resources.fonts.map((r) => ({ ...r, type: "font" })),
+ ...firstResult.resources.other.map((r) => ({ ...r, type: "other" })),
+ ].sort((a, b) => a.startTime - b.startTime);
+
+ // Take top 10 resources for waterfall
+ const topResources = allResources.slice(0, 10);
+
+ if (topResources.length > 0) {
+ const maxEndTime = Math.max(
+ ...topResources.map((r) => r.startTime + r.duration)
+ );
+ const chartWidth = 60; // Width of the waterfall bars
+
+ const waterfallTable = new Table({
+ chars: { mid: "", "left-mid": "", "mid-mid": "", "right-mid": "" },
+ colWidths: [COL_WIDTH_NAME, chartWidth + COL_WIDTH_CHART_PADDING],
+ style: { "padding-left": 1, "padding-right": 1, border: ["grey"] },
+ wordWrap: true,
+ });
+
+ waterfallTable.push([
+ color.dim("Resource"),
+ color.dim(
+ "Timeline (0ms ───────────────────────────► " +
+ formatTime(maxEndTime) +
+ ")"
+ ),
+ ]);
+
+ for (const resource of topResources) {
+ const fileName = resource.name.split("/").pop() || resource.name;
+ const shortName =
+ fileName.length > MAX_FILENAME_LENGTH
+ ? `${fileName.substring(0, TRUNCATED_FILENAME_LENGTH)}...`
+ : fileName;
+
+ const startPos = Math.floor(
+ (resource.startTime / maxEndTime) * chartWidth
+ );
+ const barLength = Math.max(
+ 1,
+ Math.floor((resource.duration / maxEndTime) * chartWidth)
+ );
+
+ const emptyBefore = " ".repeat(startPos);
+ const bar = "█".repeat(barLength);
+ const durationLabel =
+ resource.duration > maxEndTime * MIN_DURATION_THRESHOLD
+ ? formatTime(resource.duration)
+ : "";
+
+ let barColor = color.blue;
+ if (resource.isThirdParty) {
+ barColor = color.yellow;
+ }
+ if (resource.isCookieService) {
+ barColor = color.red;
+ }
+
+ waterfallTable.push([
+ color.dim(shortName),
+ `${emptyBefore + barColor(bar)} ${color.dim(durationLabel)}`,
+ ]);
+ }
+
+ console.log(waterfallTable.toString());
+ }
+ }
+
+ // ━━━ Resource Details ━━━
+ console.log(`\n${color.bold("📋 Resource Details")}`);
+
+ // Aggregate resource data across all results
+ const aggregatedResources: Array<{
+ name: string;
+ type: string;
+ source: string;
+ size: number;
+ duration: number;
+ tags: string[];
+ }> = [];
+
+ // Use first result for resource list (assuming resources are consistent)
+ const sampleResult = results[0];
+ if (sampleResult?.resources) {
+ const allSampleResources = [
+ ...sampleResult.resources.scripts.map((r) => ({
+ ...r,
+ type: "JavaScript",
+ })),
+ ...sampleResult.resources.styles.map((r) => ({ ...r, type: "CSS" })),
+ ...sampleResult.resources.images.map((r) => ({ ...r, type: "Image" })),
+ ...sampleResult.resources.fonts.map((r) => ({ ...r, type: "Font" })),
+ ...sampleResult.resources.other.map((r) => ({ ...r, type: "Other" })),
+ ];
+
+ // Calculate averages for each resource
+ for (const sampleResource of allSampleResources) {
+ const resourceName = sampleResource.name;
+
+ // Find this resource in all results and average the values
+ const avgSize =
+ results.reduce((sum, result) => {
+ const allResources = [
+ ...result.resources.scripts,
+ ...result.resources.styles,
+ ...result.resources.images,
+ ...result.resources.fonts,
+ ...result.resources.other,
+ ];
+ const found = allResources.find((r) => r.name === resourceName);
+ return sum + (found ? found.size : 0);
+ }, 0) / results.length;
+
+ const avgDuration =
+ results.reduce((sum, result) => {
+ const allResources = [
+ ...result.resources.scripts,
+ ...result.resources.styles,
+ ...result.resources.images,
+ ...result.resources.fonts,
+ ...result.resources.other,
+ ];
+ const found = allResources.find((r) => r.name === resourceName);
+ return sum + (found ? found.duration : 0);
+ }, 0) / results.length;
+
+ let source = "Bundled";
+ if (sampleResource.isThirdParty) {
+ source = sampleResource.isCookieService
+ ? "Cookie Service"
+ : "Third-Party";
+ }
+
+ const tags: string[] = [];
+ if (!sampleResource.isThirdParty) {
+ tags.push("bundled");
+ }
+ if (sampleResource.isThirdParty) {
+ tags.push("third-party");
+ }
+ if (sampleResource.isCookieService) {
+ tags.push("cookie-service");
+ }
+ if ("isDynamic" in sampleResource && sampleResource.isDynamic) {
+ tags.push("dynamic");
+ }
+
+ // Add core/other categorization for bundled scripts
+ if (
+ !sampleResource.isThirdParty &&
+ sampleResource.type === "JavaScript"
+ ) {
+ tags.push("core");
+ }
+
+ aggregatedResources.push({
+ name: resourceName,
+ type: sampleResource.type,
+ source,
+ size: avgSize,
+ duration: avgDuration,
+ tags,
+ });
+ }
+ }
+
+ // Sort by size (descending) and take top 10
+ const topResources = aggregatedResources
+ .sort((a, b) => b.size - a.size)
+ .slice(0, 10);
+
+ if (topResources.length > 0) {
+ const detailsTable = new Table({
+ head: ["Resource Name", "Type", "Source", "Size", "Duration", "Tags"],
+ colWidths: [
+ COL_WIDTH_NAME,
+ COL_WIDTH_TYPE,
+ COL_WIDTH_SOURCE,
+ COL_WIDTH_SIZE,
+ COL_WIDTH_DURATION,
+ COL_WIDTH_TAGS,
+ ],
+ style: { head: ["cyan"], border: ["grey"] },
+ wordWrap: true,
+ });
+
+ for (const resource of topResources) {
+ const fileName = resource.name.split("/").pop() || resource.name;
+ const shortName =
+ fileName.length > MAX_FILENAME_LENGTH
+ ? `${fileName.substring(0, TRUNCATED_FILENAME_LENGTH)}...`
+ : fileName;
+
+ let sourceColor = color.green;
+ if (resource.source === "Third-Party") {
+ sourceColor = color.yellow;
+ }
+ if (resource.source === "Cookie Service") {
+ sourceColor = color.red;
+ }
+
+ detailsTable.push([
+ shortName,
+ resource.type,
+ sourceColor(resource.source),
+ formatBytes(resource.size * KILOBYTE),
+ color.blue(formatTime(resource.duration)),
+ resource.tags.join(", "),
+ ]);
+ }
+
+ console.log(detailsTable.toString());
+ }
+}
+
+export async function resultsCommand(
+ logger: CliLogger,
+ appName?: string | string[]
+) {
+ logger.clear();
+ await setTimeout(ONE_SECOND);
+
+ intro(
+ `${color.bgCyan(color.black(" results "))} ${color.dim("Compare benchmarks")}`
+ );
+
+ const projectRoot = findProjectRoot();
+ const resultsDir = join(projectRoot, "benchmarks");
+ const results = await aggregateResults(logger, resultsDir);
+
+ if (Object.keys(results).length === 0) {
+ logger.error("No benchmark results found!");
+ return;
+ }
+
+ logger.debug(
+ `Found results for ${Object.keys(results).length} apps: ${Object.keys(
+ results
+ ).join(", ")}`
+ );
+
+ // If a specific app is requested, filter to that
+ let selectedApps: string[];
+
+ if (Array.isArray(appName)) {
+ // Array of app names passed (e.g., from benchmark command)
+ // Filter to only valid apps
+ selectedApps = appName.filter((name) => results[name]);
+ if (selectedApps.length === 0) {
+ logger.error("No valid results found for the specified benchmarks");
+ logger.info(`Available apps: ${Object.keys(results).join(", ")}`);
+ return;
+ }
+ } else if (appName && appName !== "__all__") {
+ // Direct command with specific app
+ if (!results[appName]) {
+ logger.error(`No results found for "${appName}"`);
+ logger.info(`Available apps: ${Object.keys(results).join(", ")}`);
+ return;
+ }
+ selectedApps = [appName];
+ } else if (appName === "__all__") {
+ // Show all results
+ selectedApps = Object.keys(results);
+ } else {
+ // Interactive mode - let user select which apps to view
+ const availableApps = Object.keys(results).sort((a, b) => {
+ if (a === "baseline") {
+ return -1;
+ }
+ if (b === "baseline") {
+ return 1;
+ }
+ return a.localeCompare(b);
+ });
+
+ const selected = await multiselect({
+ message:
+ "Select benchmarks to view (use space to toggle, all selected by default):",
+ options: availableApps.map((name) => ({
+ value: name,
+ label: name,
+ hint: `benchmarks/${name}`,
+ })),
+ initialValues: availableApps, // All selected by default
+ required: true,
+ });
+
+ if (isCancel(selected)) {
+ cancel("Operation cancelled");
+ return;
+ }
+
+ if (!Array.isArray(selected) || selected.length === 0) {
+ logger.warn("No benchmarks selected");
+ return;
+ }
+
+ selectedApps = selected;
+ }
+
+ logger.debug(`Viewing results for: ${selectedApps.join(", ")}`);
+
+ // Load configs for each app
+ const appConfigs: Record = {};
+ for (const name of Object.keys(results)) {
+ appConfigs[name] = await loadConfigForApp(logger, name, projectRoot);
+ }
+
+ // Calculate scores for each app
+ const scores: Record = {};
+ for (const [name, appResults] of Object.entries(results)) {
+ const config = appConfigs[name];
+
+ // Create app data for transparency scoring
+ const appData = {
+ name,
+ baseline: name === "baseline",
+ company: config.company ? JSON.stringify(config.company) : null,
+ techStack: JSON.stringify(config.techStack),
+ source: config.source ? JSON.stringify(config.source) : null,
+ tags: config.tags ? JSON.stringify(config.tags) : null,
+ };
+
+ scores[name] = calculateScores(
+ {
+ fcp:
+ appResults.reduce((a, b) => a + b.timing.firstContentfulPaint, 0) /
+ appResults.length,
+ lcp:
+ appResults.reduce((a, b) => a + b.timing.largestContentfulPaint, 0) /
+ appResults.length,
+ cls:
+ appResults.reduce((a, b) => a + b.timing.cumulativeLayoutShift, 0) /
+ appResults.length,
+ tbt:
+ appResults.reduce(
+ (a, b) => a + b.timing.mainThreadBlocking.total,
+ 0
+ ) / appResults.length,
+ tti:
+ appResults.reduce((a, b) => a + b.timing.timeToInteractive, 0) /
+ appResults.length,
+ timeToFirstByte:
+ appResults.reduce((a, b) => a + (b.timing.timeToFirstByte || 0), 0) /
+ appResults.length,
+ interactionToNextPaint: (() => {
+ const validValues = appResults
+ .map((result) => result.timing.interactionToNextPaint)
+ .filter(
+ (inp): inp is number =>
+ inp !== null && inp !== undefined && Number.isFinite(inp)
+ );
+ return validValues.length > 0
+ ? validValues.reduce((a, b) => a + b, 0) / validValues.length
+ : null;
+ })(),
+ },
+ {
+ totalSize:
+ appResults.reduce((a, b) => a + b.size.total, 0) / appResults.length,
+ jsSize:
+ appResults.reduce((a, b) => a + b.size.scripts.total, 0) /
+ appResults.length,
+ cssSize:
+ appResults.reduce((a, b) => a + b.size.styles, 0) / appResults.length,
+ imageSize:
+ appResults.reduce((a, b) => a + b.size.images, 0) / appResults.length,
+ fontSize:
+ appResults.reduce((a, b) => a + b.size.fonts, 0) / appResults.length,
+ otherSize:
+ appResults.reduce((a, b) => a + b.size.other, 0) / appResults.length,
+ },
+ {
+ totalRequests:
+ appResults.reduce(
+ (a, b) =>
+ a +
+ b.resources.scripts.length +
+ b.resources.styles.length +
+ b.resources.images.length +
+ b.resources.fonts.length +
+ b.resources.other.length,
+ 0
+ ) / appResults.length,
+ thirdPartyRequests:
+ appResults.reduce(
+ (a, b) =>
+ a +
+ b.resources.scripts.filter((s) => s.isThirdParty).length +
+ b.resources.styles.filter((s) => s.isThirdParty).length +
+ b.resources.images.filter((s) => s.isThirdParty).length +
+ b.resources.fonts.filter((s) => s.isThirdParty).length +
+ b.resources.other.filter((s) => s.isThirdParty).length,
+ 0
+ ) / appResults.length,
+ thirdPartySize:
+ appResults.reduce((a, b) => a + b.size.thirdParty, 0) /
+ appResults.length,
+ thirdPartyDomains:
+ appResults.reduce((sum, appResult) => {
+ const thirdPartyHosts = new Set();
+ const allThirdPartyResources = [
+ ...appResult.resources.scripts.filter((r) => r.isThirdParty),
+ ...appResult.resources.styles.filter((r) => r.isThirdParty),
+ ...appResult.resources.images.filter((r) => r.isThirdParty),
+ ...appResult.resources.fonts.filter((r) => r.isThirdParty),
+ ...appResult.resources.other.filter((r) => r.isThirdParty),
+ ];
+ for (const resource of allThirdPartyResources) {
+ try {
+ thirdPartyHosts.add(new URL(resource.name).hostname);
+ } catch {
+ // Skip invalid resource URLs
+ }
+ }
+ return sum + thirdPartyHosts.size;
+ }, 0) / appResults.length,
+ },
+ {
+ cookieBannerDetected: appResults.some(
+ (r) => r.timing.cookieBanner.detected
+ ),
+ cookieBannerVisibleTimeMs:
+ appResults.reduce(
+ (a, b) =>
+ a +
+ (b.timing.cookieBanner.userVisibleTime ??
+ b.timing.cookieBanner.visibilityTime),
+ 0
+ ) / appResults.length,
+ cookieBannerCoverage:
+ appResults.reduce(
+ (a, b) => a + b.timing.cookieBanner.viewportCoverage,
+ 0
+ ) /
+ appResults.length /
+ PERCENTAGE_DIVISOR,
+ },
+ {
+ domSize: DEFAULT_DOM_SIZE,
+ mainThreadBlocking:
+ appResults.reduce(
+ (a, b) => a + b.timing.mainThreadBlocking.total,
+ 0
+ ) / appResults.length,
+ layoutShifts:
+ appResults.reduce((a, b) => a + b.timing.cumulativeLayoutShift, 0) /
+ appResults.length,
+ },
+ name === "baseline",
+ appData,
+ appResults[0]?.timing.networkInformation
+ );
+ }
+
+ // Print detailed results for selected apps only
+ const baselineResults = results.baseline;
+ const sortedApps = selectedApps.sort((a, b) => {
+ if (a === "baseline") {
+ return -1;
+ }
+ if (b === "baseline") {
+ return 1;
+ }
+ return a.localeCompare(b);
+ });
+
+ for (const name of sortedApps) {
+ printDetailedResults(name, results[name], scores[name], baselineResults);
+ }
+}
diff --git a/packages/cookiebench-cli/src/commands/save.ts b/packages/cookiebench-cli/src/commands/save.ts
new file mode 100644
index 0000000..f78081c
--- /dev/null
+++ b/packages/cookiebench-cli/src/commands/save.ts
@@ -0,0 +1,634 @@
+import { readdir, readFile } from "node:fs/promises";
+import { join } from "node:path";
+import { setTimeout } from "node:timers/promises";
+
+import { cancel, confirm, intro, isCancel, multiselect } from "@clack/prompts";
+import type { Config } from "@consentio/runner";
+import { HALF_SECOND, PERCENTAGE_DIVISOR } from "@consentio/shared";
+import { config as loadDotenv } from "dotenv";
+import color from "picocolors";
+import type { BenchmarkScores } from "../types";
+import { isAdminUser } from "../utils/auth";
+import { findProjectRoot } from "../utils/project-root";
+import type { CliLogger } from "../utils/logger";
+import { calculateScores } from "../utils/scoring";
+import type { RawBenchmarkDetail } from "./results";
+
+// Load environment variables from .env files
+loadDotenv({ path: ".env" });
+loadDotenv({ path: ".env.local" });
+loadDotenv({ path: "www/.env.local" });
+
+type BenchmarkOutput = {
+ app: string;
+ results: RawBenchmarkDetail[];
+ scores?: BenchmarkScores;
+ metadata?: {
+ timestamp: string;
+ iterations: number;
+ languages?: string[];
+ };
+};
+
+// Benchmark result type (matching the oRPC contract)
+type BenchmarkResult = {
+ name: string;
+ baseline: boolean;
+ cookieBannerConfig: unknown;
+ techStack: unknown;
+ internationalization: unknown;
+ source: unknown;
+ includes: string[];
+ company?: string;
+ tags: string[];
+ details: RawBenchmarkDetail[];
+ average: {
+ fcp: number;
+ lcp: number;
+ cls: number;
+ tbt: number;
+ tti: number;
+ scriptLoadTime: number;
+ totalSize: number;
+ scriptSize: number;
+ resourceCount: number;
+ scriptCount: number;
+ time: number;
+ thirdPartySize: number;
+ cookieServiceSize: number;
+ bannerVisibilityTime: number;
+ viewportCoverage: number;
+ thirdPartyImpact: number;
+ mainThreadBlocking: number;
+ cookieBannerBlocking: number;
+ };
+ scores?: {
+ totalScore: number;
+ grade: "Excellent" | "Good" | "Fair" | "Poor" | "Critical";
+ categoryScores: {
+ performance: number;
+ bundleStrategy: number;
+ networkImpact: number;
+ transparency: number;
+ userExperience: number;
+ };
+ categories: Array<{
+ name: string;
+ score: number;
+ maxScore: number;
+ weight: number;
+ details: Array<{
+ metric: string;
+ value: string | number;
+ score: number;
+ maxScore: number;
+ reason: string;
+ }>;
+ status: "excellent" | "good" | "fair" | "poor";
+ }>;
+ insights: string[];
+ recommendations: string[];
+ };
+};
+
+async function saveBenchmarkResult(
+ logger: CliLogger,
+ result: BenchmarkResult
+): Promise {
+ const apiUrl = process.env.API_URL || "http://localhost:3000";
+ const endpoint = `${apiUrl}/api/orpc/benchmarks/save`;
+
+ try {
+ logger.debug(`Attempting to save ${result.name} to ${endpoint}`);
+
+ const response = await fetch(endpoint, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(result),
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ throw new Error(
+ `HTTP error! status: ${response.status}, body: ${errorText}`
+ );
+ }
+
+ const responseData = await response.json();
+ logger.success(`Saved ${result.name} (App ID: ${responseData.appId})`);
+ } catch (error) {
+ if (error instanceof Error) {
+ logger.error(`Failed to save ${result.name}: ${error.message}`);
+ if (error.message.includes("fetch failed")) {
+ logger.error(`Connection failed. Is the server running on ${apiUrl}?`);
+ }
+ } else {
+ logger.error(`Failed to save ${result.name}: Unknown error`);
+ }
+ throw error;
+ }
+}
+
+async function findResultsFiles(dir: string): Promise {
+ const files: string[] = [];
+ try {
+ const entries = await readdir(dir, { withFileTypes: true });
+
+ for (const entry of entries) {
+ const fullPath = join(dir, entry.name);
+ if (entry.isDirectory()) {
+ files.push(...(await findResultsFiles(fullPath)));
+ } else if (entry.name === "results.json") {
+ files.push(fullPath);
+ }
+ }
+ } catch {
+ // Silently fail if directory doesn't exist
+ }
+
+ return files;
+}
+
+async function loadConfigForApp(
+ logger: CliLogger,
+ appName: string,
+ projectRoot: string
+): Promise {
+ const configPath = join(projectRoot, "benchmarks", appName, "config.json");
+
+ try {
+ const configContent = await readFile(configPath, "utf-8");
+ const config = JSON.parse(configContent);
+
+ return {
+ name: config.name || appName,
+ iterations: config.iterations || 0,
+ techStack: config.techStack || {
+ languages: [],
+ frameworks: [],
+ bundler: "unknown",
+ bundleType: "bundled",
+ packageManager: "unknown",
+ typescript: false,
+ },
+ source: config.source || {
+ license: "unknown",
+ isOpenSource: false,
+ github: false,
+ npm: false,
+ },
+ includes: config.includes || { backend: [], components: [] },
+ company: config.company || undefined,
+ tags: config.tags || [],
+ cookieBanner: config.cookieBanner || {
+ serviceName: "Unknown",
+ selectors: [],
+ serviceHosts: [],
+ waitForVisibility: false,
+ measureViewportCoverage: false,
+ expectedLayoutShift: false,
+ },
+ internationalization: config.internationalization || {
+ detection: "none",
+ stringLoading: "bundled",
+ },
+ };
+ } catch (error) {
+ logger.debug(`Could not load config for ${appName}:`, error);
+ return null;
+ }
+}
+
+function transformScoresToContract(
+ scores: BenchmarkScores
+): BenchmarkResult["scores"] {
+ return {
+ totalScore: scores.totalScore,
+ grade: scores.grade,
+ categoryScores: scores.categoryScores,
+ categories: scores.categories.map((category) => ({
+ name: category.name,
+ score: category.score,
+ maxScore: category.maxScore,
+ weight: category.weight,
+ details: category.details.map((detail) => ({
+ metric: detail.name,
+ value: detail.value ?? detail.score,
+ score: detail.score,
+ maxScore: detail.maxScore,
+ reason: detail.reason,
+ })),
+ status: category.status,
+ })),
+ insights: scores.insights,
+ recommendations: scores.recommendations,
+ };
+}
+
+export async function saveCommand(
+ logger: CliLogger,
+ appName?: string
+): Promise {
+ const projectRoot = findProjectRoot();
+
+ // Double-check admin access (safeguard)
+ if (!isAdminUser()) {
+ logger.error("This command requires admin access");
+ process.exit(1);
+ }
+
+ logger.clear();
+ await setTimeout(HALF_SECOND);
+
+ intro(
+ `${color.bgBlue(color.white(" save "))} ${color.dim("Sync results to database")}`
+ );
+
+ // Check database configuration
+ const databaseUrl =
+ process.env.DATABASE_URL || process.env.TURSO_DATABASE_URL;
+ const authToken =
+ process.env.DATABASE_AUTH_TOKEN || process.env.TURSO_AUTH_TOKEN;
+ const apiUrl = process.env.API_URL || "http://localhost:3000";
+
+ logger.info(`API endpoint: ${color.cyan(apiUrl)}`);
+
+ if (
+ databaseUrl?.startsWith("libsql://") ||
+ databaseUrl?.startsWith("wss://")
+ ) {
+ logger.info(
+ `Database: ${color.cyan(`Turso (${databaseUrl.split("@")[0]}@***)`)}`
+ );
+ if (!authToken) {
+ logger.warn("⚠️ No auth token found. Database operations may fail.");
+ }
+ } else if (databaseUrl?.startsWith("file:")) {
+ logger.info(`Database: ${color.cyan(`Local (${databaseUrl})`)}`);
+ } else {
+ logger.info(`Database: ${color.cyan("Local SQLite (benchmarks.db)")}`);
+ }
+
+ const resultsDir = join(projectRoot, "benchmarks");
+ const resultsFiles = await findResultsFiles(resultsDir);
+
+ if (resultsFiles.length === 0) {
+ logger.error("No benchmark results found!");
+ logger.info(
+ `Run ${color.cyan("cookiebench benchmark")} first to generate results.`
+ );
+ return;
+ }
+
+ logger.info(`Found ${resultsFiles.length} results file(s)`);
+
+ // Load all results
+ const allResults: Record = {};
+ for (const file of resultsFiles) {
+ try {
+ const content = await readFile(file, "utf-8");
+ const data: BenchmarkOutput = JSON.parse(content);
+
+ if (data.app && data.results) {
+ allResults[data.app] = data;
+ }
+ } catch (error) {
+ logger.debug(`Failed to load ${file}:`, error);
+ }
+ }
+
+ if (Object.keys(allResults).length === 0) {
+ logger.error("No valid benchmark results found!");
+ return;
+ }
+
+ // If specific app requested, save only that one
+ if (appName) {
+ const result = allResults[appName];
+ if (!result) {
+ logger.error(`No results found for app: ${appName}`);
+ logger.info(`Available apps: ${Object.keys(allResults).join(", ")}`);
+ return;
+ }
+
+ await saveAppToDatabase(logger, appName, result, projectRoot);
+ logger.outro("Done!");
+ return;
+ }
+
+ // Otherwise, show interactive selection
+ const appOptions = Object.keys(allResults).map((name) => ({
+ value: name,
+ label: name,
+ hint: `${allResults[name].results.length} iterations`,
+ }));
+
+ appOptions.push({
+ value: "__all__",
+ label: "Save all apps",
+ hint: "Sync all benchmark results to database",
+ });
+
+ const selectedApps = await multiselect({
+ message: "Select benchmarks to save to database:",
+ options: appOptions,
+ required: true,
+ });
+
+ if (isCancel(selectedApps)) {
+ cancel("Operation cancelled");
+ return;
+ }
+
+ if (!Array.isArray(selectedApps) || selectedApps.length === 0) {
+ logger.warn("No benchmarks selected");
+ return;
+ }
+
+ // Confirm before saving
+ const appsToSave = selectedApps.includes("__all__")
+ ? Object.keys(allResults)
+ : (selectedApps as string[]);
+
+ const confirmBeforeSave = await confirm({
+ message: `Save ${appsToSave.length} benchmark(s) to ${apiUrl}?`,
+ initialValue: true,
+ });
+
+ if (isCancel(confirmBeforeSave) || !confirmBeforeSave) {
+ cancel("Operation cancelled");
+ return;
+ }
+
+ // Save selected apps
+ let savedCount = 0;
+ let errorCount = 0;
+
+ for (const name of appsToSave) {
+ try {
+ await saveAppToDatabase(logger, name, allResults[name], projectRoot);
+ savedCount += 1;
+ } catch (error) {
+ if (error instanceof Error) {
+ logger.error(`Failed to save ${name}: ${error.message}`);
+ } else {
+ logger.error(`Failed to save ${name}: Unknown error`);
+ }
+ errorCount += 1;
+ }
+ }
+
+ // Summary
+ logger.message("");
+ if (savedCount > 0) {
+ logger.success(`Successfully saved ${savedCount} app(s)`);
+ }
+ if (errorCount > 0) {
+ logger.warn(`Failed to save ${errorCount} app(s)`);
+ }
+
+ logger.outro(
+ `Saved ${savedCount}/${appsToSave.length} benchmarks to database`
+ );
+}
+
+async function saveAppToDatabase(
+ logger: CliLogger,
+ appName: string,
+ result: BenchmarkOutput,
+ projectRoot: string
+): Promise {
+ const appConfig = await loadConfigForApp(logger, appName, projectRoot);
+ const appResults = result.results;
+ if (appResults.length === 0) {
+ logger.warn(`Skipping ${appName}: no benchmark iterations found.`);
+ return;
+ }
+
+ // Calculate scores if not already in results
+ let scores = result.scores;
+ if (!scores) {
+ const appData = {
+ name: appName,
+ baseline: appName === "baseline",
+ company: appConfig?.company ? JSON.stringify(appConfig.company) : null,
+ techStack: appConfig?.techStack
+ ? JSON.stringify(appConfig.techStack)
+ : "{}",
+ source: appConfig?.source ? JSON.stringify(appConfig.source) : null,
+ tags: appConfig?.tags ? JSON.stringify(appConfig.tags) : null,
+ };
+
+ scores = calculateScores(
+ {
+ fcp:
+ appResults.reduce((a, b) => a + b.timing.firstContentfulPaint, 0) /
+ appResults.length,
+ lcp:
+ appResults.reduce((a, b) => a + b.timing.largestContentfulPaint, 0) /
+ appResults.length,
+ cls:
+ appResults.reduce((a, b) => a + b.timing.cumulativeLayoutShift, 0) /
+ appResults.length,
+ tbt:
+ appResults.reduce(
+ (a, b) => a + b.timing.mainThreadBlocking.total,
+ 0
+ ) / appResults.length,
+ tti:
+ appResults.reduce((a, b) => a + b.timing.timeToInteractive, 0) /
+ appResults.length,
+ timeToFirstByte:
+ appResults.reduce((a, b) => a + (b.timing.timeToFirstByte || 0), 0) /
+ appResults.length,
+ interactionToNextPaint: (() => {
+ const validValues = appResults
+ .map((resultItem) => resultItem.timing.interactionToNextPaint)
+ .filter(
+ (inp): inp is number =>
+ inp !== null && inp !== undefined && Number.isFinite(inp)
+ );
+ return validValues.length > 0
+ ? validValues.reduce((a, b) => a + b, 0) / validValues.length
+ : null;
+ })(),
+ },
+ {
+ totalSize:
+ appResults.reduce((a, b) => a + b.size.total, 0) / appResults.length,
+ jsSize:
+ appResults.reduce((a, b) => a + b.size.scripts.total, 0) /
+ appResults.length,
+ cssSize:
+ appResults.reduce((a, b) => a + b.size.styles, 0) / appResults.length,
+ imageSize:
+ appResults.reduce((a, b) => a + b.size.images, 0) / appResults.length,
+ fontSize:
+ appResults.reduce((a, b) => a + b.size.fonts, 0) / appResults.length,
+ otherSize:
+ appResults.reduce((a, b) => a + b.size.other, 0) / appResults.length,
+ },
+ {
+ totalRequests:
+ appResults.reduce(
+ (a, b) =>
+ a +
+ b.resources.scripts.length +
+ b.resources.styles.length +
+ b.resources.images.length +
+ b.resources.fonts.length +
+ b.resources.other.length,
+ 0
+ ) / appResults.length,
+ thirdPartyRequests:
+ appResults.reduce(
+ (a, b) =>
+ a +
+ b.resources.scripts.filter((s) => s.isThirdParty).length +
+ b.resources.styles.filter((s) => s.isThirdParty).length +
+ b.resources.images.filter((s) => s.isThirdParty).length +
+ b.resources.fonts.filter((s) => s.isThirdParty).length +
+ b.resources.other.filter((s) => s.isThirdParty).length,
+ 0
+ ) / appResults.length,
+ thirdPartySize:
+ appResults.reduce((a, b) => a + b.size.thirdParty, 0) /
+ appResults.length,
+ thirdPartyDomains:
+ appResults.reduce((sum, appResult) => {
+ const thirdPartyHosts = new Set();
+ const allThirdPartyResources = [
+ ...appResult.resources.scripts.filter((r) => r.isThirdParty),
+ ...appResult.resources.styles.filter((r) => r.isThirdParty),
+ ...appResult.resources.images.filter((r) => r.isThirdParty),
+ ...appResult.resources.fonts.filter((r) => r.isThirdParty),
+ ...appResult.resources.other.filter((r) => r.isThirdParty),
+ ];
+ for (const resource of allThirdPartyResources) {
+ try {
+ thirdPartyHosts.add(new URL(resource.name).hostname);
+ } catch {
+ // Ignore invalid URLs
+ }
+ }
+ return sum + thirdPartyHosts.size;
+ }, 0) / appResults.length,
+ },
+ {
+ cookieBannerDetected: appResults.some(
+ (r) => r.timing.cookieBanner.detected
+ ),
+ cookieBannerVisibleTimeMs:
+ appResults.reduce(
+ (a, b) =>
+ a +
+ (b.timing.cookieBanner.userVisibleTime ??
+ b.timing.cookieBanner.visibilityTime),
+ 0
+ ) / appResults.length,
+ cookieBannerCoverage:
+ appResults.reduce(
+ (a, b) => a + b.timing.cookieBanner.viewportCoverage,
+ 0
+ ) /
+ appResults.length /
+ PERCENTAGE_DIVISOR,
+ },
+ {
+ domSize: 1500,
+ mainThreadBlocking:
+ appResults.reduce(
+ (a, b) => a + b.timing.mainThreadBlocking.total,
+ 0
+ ) / appResults.length,
+ layoutShifts:
+ appResults.reduce((a, b) => a + b.timing.cumulativeLayoutShift, 0) /
+ appResults.length,
+ },
+ appName === "baseline",
+ appData,
+ appResults[0]?.timing.networkInformation
+ );
+ }
+
+ // Convert to API format
+ const benchmarkResult: BenchmarkResult = {
+ name: appName,
+ baseline: appName === "baseline",
+ cookieBannerConfig: appConfig?.cookieBanner || {},
+ techStack: appConfig?.techStack || {},
+ internationalization: appConfig?.internationalization || {},
+ source: appConfig?.source || {},
+ includes: appConfig?.includes
+ ? Object.values(appConfig.includes)
+ .flat()
+ .filter((v): v is string => typeof v === "string")
+ : [],
+ company: appConfig?.company ? JSON.stringify(appConfig.company) : undefined,
+ tags: appConfig?.tags || [],
+ details: appResults,
+ average: {
+ fcp:
+ appResults.reduce((a, b) => a + b.timing.firstContentfulPaint, 0) /
+ appResults.length,
+ lcp:
+ appResults.reduce((a, b) => a + b.timing.largestContentfulPaint, 0) /
+ appResults.length,
+ cls:
+ appResults.reduce((a, b) => a + b.timing.cumulativeLayoutShift, 0) /
+ appResults.length,
+ tbt:
+ appResults.reduce((a, b) => a + b.timing.mainThreadBlocking.total, 0) /
+ appResults.length,
+ tti:
+ appResults.reduce((a, b) => a + b.timing.timeToInteractive, 0) /
+ appResults.length,
+ scriptLoadTime: 0,
+ totalSize:
+ appResults.reduce((a, b) => a + b.size.total, 0) / appResults.length,
+ scriptSize: 0,
+ resourceCount:
+ appResults.reduce((a, b) => a + b.resources.scripts.length, 0) /
+ appResults.length,
+ scriptCount:
+ appResults.reduce((a, b) => a + b.resources.scripts.length, 0) /
+ appResults.length,
+ time: appResults.reduce((a, b) => a + b.duration, 0) / appResults.length,
+ thirdPartySize:
+ appResults.reduce((a, b) => a + b.size.thirdParty, 0) /
+ appResults.length,
+ cookieServiceSize:
+ appResults.reduce((a, b) => a + b.size.cookieServices, 0) /
+ appResults.length,
+ bannerVisibilityTime:
+ appResults.reduce(
+ (a, b) =>
+ a +
+ (b.timing.cookieBanner.userVisibleTime ??
+ b.timing.cookieBanner.visibilityTime),
+ 0
+ ) / appResults.length,
+ viewportCoverage:
+ appResults.reduce(
+ (a, b) => a + b.timing.cookieBanner.viewportCoverage,
+ 0
+ ) / appResults.length,
+ thirdPartyImpact:
+ appResults.reduce((a, b) => a + b.timing.thirdParty.totalImpact, 0) /
+ appResults.length,
+ mainThreadBlocking:
+ appResults.reduce((a, b) => a + b.timing.mainThreadBlocking.total, 0) /
+ appResults.length,
+ cookieBannerBlocking:
+ appResults.reduce(
+ (a, b) => a + b.timing.mainThreadBlocking.cookieBannerEstimate,
+ 0
+ ) / appResults.length,
+ },
+ scores: scores ? transformScoresToContract(scores) : undefined,
+ };
+
+ await saveBenchmarkResult(logger, benchmarkResult);
+}
diff --git a/packages/cookiebench-cli/src/commands/scores.ts b/packages/cookiebench-cli/src/commands/scores.ts
new file mode 100644
index 0000000..b3d83ab
--- /dev/null
+++ b/packages/cookiebench-cli/src/commands/scores.ts
@@ -0,0 +1,368 @@
+import { readdir, readFile } from "node:fs/promises";
+import { join } from "node:path";
+import { setTimeout } from "node:timers/promises";
+import { intro, isCancel, select } from "@clack/prompts";
+import type { Config } from "@consentio/runner";
+import { HALF_SECOND, PERCENTAGE_DIVISOR } from "@consentio/shared";
+import color from "picocolors";
+import type { BenchmarkScores } from "../types";
+import { DEFAULT_DOM_SIZE } from "../utils/constants";
+import { findProjectRoot } from "../utils/project-root";
+import type { CliLogger } from "../utils/logger";
+import { calculateScores, printScores } from "../utils/scoring";
+import type { RawBenchmarkDetail } from "./results";
+
+type BenchmarkOutput = {
+ app: string;
+ results: RawBenchmarkDetail[];
+ scores?: BenchmarkScores;
+ metadata?: {
+ timestamp: string;
+ iterations: number;
+ languages?: string[];
+ };
+};
+
+async function findResultsFiles(dir: string): Promise {
+ const files: string[] = [];
+ try {
+ const entries = await readdir(dir, { withFileTypes: true });
+
+ for (const entry of entries) {
+ const fullPath = join(dir, entry.name);
+ if (entry.isDirectory()) {
+ files.push(...(await findResultsFiles(fullPath)));
+ } else if (entry.name === "results.json") {
+ files.push(fullPath);
+ }
+ }
+ } catch {
+ // Directory doesn't exist or can't be read
+ }
+
+ return files;
+}
+
+async function loadConfigForApp(
+ logger: CliLogger,
+ appName: string,
+ projectRoot: string
+): Promise {
+ const configPath = join(projectRoot, "benchmarks", appName, "config.json");
+
+ try {
+ const configContent = await readFile(configPath, "utf-8");
+ const config = JSON.parse(configContent);
+
+ return {
+ name: config.name || appName,
+ iterations: config.iterations || 0,
+ techStack: config.techStack || {
+ languages: [],
+ frameworks: [],
+ bundler: "unknown",
+ bundleType: "unknown",
+ packageManager: "unknown",
+ typescript: false,
+ },
+ source: config.source || {
+ license: "unknown",
+ isOpenSource: false,
+ github: false,
+ npm: false,
+ },
+ includes: config.includes || { backend: [], components: [] },
+ company: config.company || undefined,
+ tags: config.tags || [],
+ cookieBanner: config.cookieBanner || {
+ serviceName: "Unknown",
+ selectors: [],
+ serviceHosts: [],
+ waitForVisibility: false,
+ measureViewportCoverage: false,
+ expectedLayoutShift: false,
+ },
+ internationalization: config.internationalization || {
+ detection: "none",
+ stringLoading: "bundled",
+ },
+ };
+ } catch (error) {
+ logger.debug(`Could not load config for ${appName}:`, error);
+ return null;
+ }
+}
+
+export async function scoresCommand(logger: CliLogger, appName?: string) {
+ logger.clear();
+ await setTimeout(HALF_SECOND);
+
+ intro(`${color.bgCyan(color.black(" scores "))}`);
+
+ const projectRoot = findProjectRoot();
+ const resultsDir = join(projectRoot, "benchmarks");
+ const resultsFiles = await findResultsFiles(resultsDir);
+
+ if (resultsFiles.length === 0) {
+ logger.error("No benchmark results found!");
+ logger.info(
+ `Run ${color.cyan("cookiebench benchmark")} first to generate results.`
+ );
+ return;
+ }
+
+ logger.debug(`Found ${resultsFiles.length} results files`);
+
+ // Load all results
+ const allResults: Record = {};
+ for (const file of resultsFiles) {
+ try {
+ const content = await readFile(file, "utf-8");
+ const data: BenchmarkOutput = JSON.parse(content);
+
+ if (data.app && data.results) {
+ allResults[data.app] = data;
+ }
+ } catch (error) {
+ logger.debug(`Failed to load ${file}:`, error);
+ }
+ }
+
+ if (Object.keys(allResults).length === 0) {
+ logger.error("No valid benchmark results found!");
+ return;
+ }
+
+ // If specific app requested, show only that one
+ if (appName) {
+ const result = allResults[appName];
+ if (!result) {
+ logger.error(`No results found for app: ${appName}`);
+ logger.info(`Available apps: ${Object.keys(allResults).join(", ")}`);
+ return;
+ }
+
+ await displayAppScores(logger, appName, result);
+ return;
+ }
+
+ // Otherwise, show interactive selection
+ const appOptions = Object.keys(allResults).map((name) => ({
+ value: name,
+ label: name,
+ hint: `${allResults[name].results.length} iterations`,
+ }));
+
+ appOptions.push({
+ value: "__all__",
+ label: "Show all apps",
+ hint: "Display scores for all benchmarks",
+ });
+
+ const selectedApp = await select({
+ message: "Which benchmark scores would you like to view?",
+ options: appOptions,
+ });
+
+ if (isCancel(selectedApp)) {
+ logger.info("Operation cancelled");
+ return;
+ }
+
+ if (selectedApp === "__all__") {
+ // Show all apps
+ for (const [name, result] of Object.entries(allResults)) {
+ await displayAppScores(logger, name, result);
+ logger.message(""); // Add spacing between apps
+ }
+ } else {
+ await displayAppScores(
+ logger,
+ selectedApp as string,
+ allResults[selectedApp as string]
+ );
+ }
+
+ logger.outro("Done!");
+}
+
+async function displayAppScores(
+ logger: CliLogger,
+ appName: string,
+ result: BenchmarkOutput
+) {
+ logger.info(`\n${color.bold(color.cyan(`📊 ${appName}`))}`);
+
+ // Show metadata if available
+ if (result.metadata) {
+ logger.debug(`Iterations: ${result.metadata.iterations}`);
+ logger.debug(`Timestamp: ${result.metadata.timestamp}`);
+ }
+
+ // If scores are already calculated and stored, use them
+ if (result.scores) {
+ logger.debug("Using pre-calculated scores from results file");
+ printScores(result.scores);
+ return;
+ }
+
+ // Otherwise, calculate scores from raw results
+ logger.debug("Calculating scores from raw benchmark data");
+
+ const appResults = result.results;
+ if (appResults.length === 0) {
+ logger.warn(
+ `No benchmark runs found for ${appName}, skipping score output.`
+ );
+ return;
+ }
+ const projectRoot = findProjectRoot();
+ const config = await loadConfigForApp(logger, appName, projectRoot);
+
+ if (!config) {
+ logger.warn(`Could not load config for ${appName}, using default values`);
+ }
+
+ // Create app data for transparency scoring
+ const appData = {
+ name: appName,
+ baseline: appName === "baseline",
+ company: config?.company ? JSON.stringify(config.company) : null,
+ techStack: config?.techStack ? JSON.stringify(config.techStack) : "{}",
+ source: config?.source ? JSON.stringify(config.source) : null,
+ tags: config?.tags ? JSON.stringify(config.tags) : null,
+ };
+
+ const scores = calculateScores(
+ {
+ fcp:
+ appResults.reduce((a, b) => a + b.timing.firstContentfulPaint, 0) /
+ appResults.length,
+ lcp:
+ appResults.reduce((a, b) => a + b.timing.largestContentfulPaint, 0) /
+ appResults.length,
+ cls:
+ appResults.reduce((a, b) => a + b.timing.cumulativeLayoutShift, 0) /
+ appResults.length,
+ tbt:
+ appResults.reduce((a, b) => a + b.timing.mainThreadBlocking.total, 0) /
+ appResults.length,
+ tti:
+ appResults.reduce((a, b) => a + b.timing.timeToInteractive, 0) /
+ appResults.length,
+ timeToFirstByte:
+ appResults.reduce((a, b) => a + (b.timing.timeToFirstByte || 0), 0) /
+ appResults.length,
+ interactionToNextPaint: (() => {
+ const validValues = appResults
+ .map((r) => r.timing.interactionToNextPaint)
+ .filter(
+ (inp): inp is number =>
+ inp !== null && inp !== undefined && Number.isFinite(inp)
+ );
+ return validValues.length > 0
+ ? validValues.reduce((a, b) => a + b, 0) / validValues.length
+ : null;
+ })(),
+ },
+ {
+ totalSize:
+ appResults.reduce((a, b) => a + b.size.total, 0) / appResults.length,
+ jsSize:
+ appResults.reduce((a, b) => a + b.size.scripts.total, 0) /
+ appResults.length,
+ cssSize:
+ appResults.reduce((a, b) => a + b.size.styles, 0) / appResults.length,
+ imageSize:
+ appResults.reduce((a, b) => a + b.size.images, 0) / appResults.length,
+ fontSize:
+ appResults.reduce((a, b) => a + b.size.fonts, 0) / appResults.length,
+ otherSize:
+ appResults.reduce((a, b) => a + b.size.other, 0) / appResults.length,
+ },
+ {
+ totalRequests:
+ appResults.reduce(
+ (a, b) =>
+ a +
+ b.resources.scripts.length +
+ b.resources.styles.length +
+ b.resources.images.length +
+ b.resources.fonts.length +
+ b.resources.other.length,
+ 0
+ ) / appResults.length,
+ thirdPartyRequests:
+ appResults.reduce(
+ (a, b) =>
+ a +
+ b.resources.scripts.filter((s) => s.isThirdParty).length +
+ b.resources.styles.filter((s) => s.isThirdParty).length +
+ b.resources.images.filter((s) => s.isThirdParty).length +
+ b.resources.fonts.filter((s) => s.isThirdParty).length +
+ b.resources.other.filter((s) => s.isThirdParty).length,
+ 0
+ ) / appResults.length,
+ thirdPartySize:
+ appResults.reduce((a, b) => a + b.size.thirdParty, 0) /
+ appResults.length,
+ thirdPartyDomains: (() => {
+ // Calculate unique third-party domains from resources
+ const thirdPartyHosts = new Set();
+ for (const appResult of appResults) {
+ // Check all resource types for third-party resources
+ const allThirdPartyResources = [
+ ...appResult.resources.scripts.filter((r) => r.isThirdParty),
+ ...appResult.resources.styles.filter((r) => r.isThirdParty),
+ ...appResult.resources.images.filter((r) => r.isThirdParty),
+ ...appResult.resources.fonts.filter((r) => r.isThirdParty),
+ ...appResult.resources.other.filter((r) => r.isThirdParty),
+ ];
+ for (const resource of allThirdPartyResources) {
+ try {
+ const url = new URL(resource.name);
+ thirdPartyHosts.add(url.hostname);
+ } catch {
+ // Invalid URL, skip
+ }
+ }
+ }
+ return thirdPartyHosts.size;
+ })(),
+ },
+ {
+ cookieBannerDetected: appResults.some(
+ (r) => r.timing.cookieBanner.detected
+ ),
+ cookieBannerVisibleTimeMs:
+ appResults.reduce(
+ (a, b) =>
+ a +
+ (b.timing.cookieBanner.userVisibleTime ??
+ b.timing.cookieBanner.visibilityTime),
+ 0
+ ) / appResults.length,
+ cookieBannerCoverage:
+ appResults.reduce(
+ (a, b) => a + b.timing.cookieBanner.viewportCoverage,
+ 0
+ ) /
+ appResults.length /
+ PERCENTAGE_DIVISOR,
+ },
+ {
+ domSize: DEFAULT_DOM_SIZE,
+ mainThreadBlocking:
+ appResults.reduce((a, b) => a + b.timing.mainThreadBlocking.total, 0) /
+ appResults.length,
+ layoutShifts:
+ appResults.reduce((a, b) => a + b.timing.cumulativeLayoutShift, 0) /
+ appResults.length,
+ },
+ appName === "baseline",
+ appData,
+ appResults[0]?.timing.networkInformation
+ );
+
+ printScores(scores);
+}
diff --git a/packages/cookiebench-cli/src/components/intro.ts b/packages/cookiebench-cli/src/components/intro.ts
new file mode 100644
index 0000000..1afb821
--- /dev/null
+++ b/packages/cookiebench-cli/src/components/intro.ts
@@ -0,0 +1,94 @@
+/** biome-ignore-all lint/style/noMagicNumbers: its okay for the figlet word */
+import figlet from "figlet";
+import color from "picocolors";
+import type { CliLogger } from "../utils/logger";
+
+/**
+ * Displays the CLI introduction sequence with figlet art
+ * @param logger - The CLI logger instance
+ * @param version - The CLI version string
+ */
+export async function displayIntro(
+ logger: CliLogger,
+ version?: string
+): Promise {
+ // Generate and display Figlet text (async)
+ let figletText = "cookiebench"; // Default
+ try {
+ figletText = await new Promise((resolve) => {
+ figlet.text(
+ "cookiebench",
+ {
+ font: "Slant",
+ horizontalLayout: "default",
+ verticalLayout: "default",
+ width: 80,
+ whitespaceBreak: true,
+ },
+ (err, data) => {
+ if (err) {
+ logger.debug("Failed to generate figlet text");
+ resolve("cookiebench");
+ } else {
+ resolve(data || "cookiebench");
+ }
+ }
+ );
+ });
+ } catch (error) {
+ logger.debug("Error generating figlet text", error);
+ }
+
+ // Display the figlet text with cyan/teal gradient
+ const customColor = {
+ cyan10: (text: string) => `\x1b[38;2;10;80;90m${text}\x1b[0m`,
+ cyan20: (text: string) => `\x1b[38;2;15;100;110m${text}\x1b[0m`,
+ cyan30: (text: string) => `\x1b[38;2;20;120;130m${text}\x1b[0m`,
+ cyan40: (text: string) => `\x1b[38;2;25;150;170m${text}\x1b[0m`,
+ cyan50: (text: string) => `\x1b[38;2;30;170;190m${text}\x1b[0m`,
+ cyan75: (text: string) => `\x1b[38;2;34;211;230m${text}\x1b[0m`,
+ cyan90: (text: string) => `\x1b[38;2;45;225;245m${text}\x1b[0m`,
+ cyan100: (text: string) => `\x1b[38;2;65;235;255m${text}\x1b[0m`,
+ };
+
+ const lines = figletText.split("\n");
+ const gradientDenominator = Math.max(1, lines.length - 1);
+ const coloredLines = lines.map((line, index) => {
+ // Calculate the position in the gradient based on line index
+ const position = index / gradientDenominator;
+
+ if (position < 0.1) {
+ return customColor.cyan10(line);
+ }
+ if (position < 0.2) {
+ return customColor.cyan20(line);
+ }
+ if (position < 0.3) {
+ return customColor.cyan30(line);
+ }
+ if (position < 0.4) {
+ return customColor.cyan40(line);
+ }
+ if (position < 0.5) {
+ return customColor.cyan50(line);
+ }
+ if (position < 0.65) {
+ return customColor.cyan75(line);
+ }
+ if (position < 0.8) {
+ return customColor.cyan90(line);
+ }
+ return customColor.cyan100(line);
+ });
+
+ // Join all colored lines and send as a single message
+ logger.message(coloredLines.join("\n"));
+
+ // Display version if provided
+ if (version) {
+ logger.message(color.dim(`v${version}`));
+ }
+
+ // Spacing before next step
+ logger.message("");
+}
diff --git a/packages/cookiebench-cli/src/index.ts b/packages/cookiebench-cli/src/index.ts
new file mode 100644
index 0000000..3317c4e
--- /dev/null
+++ b/packages/cookiebench-cli/src/index.ts
@@ -0,0 +1,146 @@
+#!/usr/bin/env node
+import { setTimeout } from "node:timers/promises";
+import { cancel, isCancel, select } from "@clack/prompts";
+import { HALF_SECOND } from "@consentio/shared";
+import { benchmarkCommand } from "./commands/benchmark";
+import { dbCommand } from "./commands/db";
+import { resultsCommand } from "./commands/results";
+import { saveCommand } from "./commands/save";
+import { scoresCommand } from "./commands/scores";
+import { displayIntro } from "./components/intro";
+import { isAdminUser } from "./utils/auth";
+import { type CliLogger, createCliLogger } from "./utils/logger";
+
+// Get log level from env or default to info
+const logLevel =
+ (process.env.LOG_LEVEL as "error" | "warn" | "info" | "debug") || "info";
+const logger: CliLogger = createCliLogger(logLevel);
+
+// Check admin access for restricted commands
+const isAdmin = isAdminUser();
+
+function onCancel() {
+ cancel("Operation cancelled.");
+ process.exit(0);
+}
+
+async function main() {
+ logger.clear();
+ await setTimeout(HALF_SECOND);
+
+ // Check for command line arguments
+ const rawArgs = process.argv.slice(2);
+ const args = rawArgs[0] === "--" ? rawArgs.slice(1) : rawArgs;
+ const command = args[0];
+
+ // Show intro for interactive mode
+ if (!command) {
+ await displayIntro(logger);
+ }
+
+ // If no command specified, show the prompt
+ if (command) {
+ // Direct command execution
+ switch (command) {
+ case "benchmark":
+ await benchmarkCommand(logger, args[1]);
+ break;
+ case "results":
+ await resultsCommand(logger, args[1]);
+ break;
+ case "scores":
+ await scoresCommand(logger, args[1]);
+ break;
+ case "save":
+ if (!isAdmin) {
+ logger.error("This command requires admin access");
+ process.exit(1);
+ }
+ await saveCommand(logger, args[1]);
+ break;
+ case "db":
+ if (!isAdmin) {
+ logger.error("This command requires admin access");
+ process.exit(1);
+ }
+ await dbCommand(logger, args[1]);
+ break;
+ default: {
+ logger.error(`Unknown command: ${command}`);
+ const availableCommands = ["benchmark", "results", "scores"];
+ if (isAdmin) {
+ availableCommands.push("save", "db");
+ }
+ logger.info(`Available commands: ${availableCommands.join(", ")}`);
+ process.exit(1);
+ }
+ }
+ process.exit(0);
+ } else {
+ // Build options based on admin access
+ const options = [
+ {
+ value: "benchmark",
+ label: "Run a benchmark",
+ hint: "Run a performance benchmark on a URL",
+ },
+ {
+ value: "results",
+ label: "Results",
+ hint: "View detailed benchmark results",
+ },
+ {
+ value: "scores",
+ label: "Scores",
+ hint: "View score-focused benchmark output",
+ },
+ ];
+
+ // Add admin-only commands
+ if (isAdmin) {
+ options.push({
+ value: "save",
+ label: "Save to database",
+ hint: "🔒 Admin: Sync benchmark results to database",
+ });
+ options.push({
+ value: "db",
+ label: "Database",
+ hint: "🔒 Admin: Manage database schema and migrations",
+ });
+ }
+
+ const selectedCommand = await select({
+ message: "What would you like to do?",
+ options,
+ });
+
+ if (isCancel(selectedCommand)) {
+ return onCancel();
+ }
+
+ // biome-ignore lint/style/useDefaultSwitchClause: this is a CLI tool
+ switch (selectedCommand) {
+ case "benchmark":
+ await benchmarkCommand(logger);
+ break;
+ case "results":
+ await resultsCommand(logger);
+ break;
+ case "scores":
+ await scoresCommand(logger);
+ break;
+ case "save":
+ await saveCommand(logger);
+ break;
+ case "db":
+ await dbCommand(logger);
+ break;
+ }
+ }
+}
+
+main().catch((error) => {
+ logger.error("Fatal error:", error);
+ process.exit(1);
+});
diff --git a/packages/cookiebench-cli/src/types/index.ts b/packages/cookiebench-cli/src/types/index.ts
new file mode 100644
index 0000000..639d33e
--- /dev/null
+++ b/packages/cookiebench-cli/src/types/index.ts
@@ -0,0 +1,39 @@
+// Re-export types from runner package
+export type {
+ BenchmarkDetails,
+ BenchmarkResult,
+ Config,
+ ServerInfo,
+} from "@consentio/runner";
+
+// CLI-specific scoring types
+export type BenchmarkScores = {
+ totalScore: number;
+ grade: "Excellent" | "Good" | "Fair" | "Poor" | "Critical";
+ categoryScores: {
+ performance: number;
+ bundleStrategy: number;
+ networkImpact: number;
+ transparency: number;
+ userExperience: number;
+ };
+ categories: Array<{
+ name: string;
+ score: number;
+ maxScore: number;
+ weight: number;
+ details: Array<{
+ name: string;
+ value?: string | number;
+ score: number;
+ maxScore: number;
+ weight: number;
+ status: "excellent" | "good" | "fair" | "poor";
+ reason: string;
+ }>;
+ status: "excellent" | "good" | "fair" | "poor";
+ reason: string;
+ }>;
+ insights: string[];
+ recommendations: string[];
+};
diff --git a/packages/cookiebench-cli/src/utils/auth.ts b/packages/cookiebench-cli/src/utils/auth.ts
new file mode 100644
index 0000000..ac23516
--- /dev/null
+++ b/packages/cookiebench-cli/src/utils/auth.ts
@@ -0,0 +1,11 @@
+/**
+ * Check if the user has admin access for database operations
+ * This is gated by an admin flag to prevent unauthorized database writes
+ */
+export function isAdminUser(): boolean {
+ // Check for admin flag in environment
+ const adminFlag = (process.env.CONSENT_ADMIN || "").trim().toLowerCase();
+
+ // Accept 'true', '1', 'yes' as valid values
+ return adminFlag === "true" || adminFlag === "1" || adminFlag === "yes";
+}
diff --git a/packages/cookiebench-cli/src/utils/constants.ts b/packages/cookiebench-cli/src/utils/constants.ts
new file mode 100644
index 0000000..4784dfd
--- /dev/null
+++ b/packages/cookiebench-cli/src/utils/constants.ts
@@ -0,0 +1,31 @@
+// CLI-specific constants
+export const DEFAULT_ITERATIONS = 5;
+export const DEFAULT_DOM_SIZE = 1500;
+export const DEFAULT_THIRD_PARTY_DOMAINS = 5;
+export const SEPARATOR_WIDTH = 80;
+export const LINE_LENGTH = 80;
+export const CLS_DECIMAL_PLACES = 3;
+
+// Score thresholds
+export const SCORE_THRESHOLD_POOR = 70;
+export const SCORE_THRESHOLD_FAIR = 90;
+
+// CLS thresholds (Core Web Vitals)
+export const CLS_THRESHOLD_GOOD = 0.1;
+export const CLS_THRESHOLD_FAIR = 0.25;
+
+// Table column widths
+export const COL_WIDTH_NAME = 25;
+export const COL_WIDTH_TYPE = 12;
+export const COL_WIDTH_SOURCE = 15;
+export const COL_WIDTH_SIZE = 10;
+export const COL_WIDTH_DURATION = 10;
+export const COL_WIDTH_TAGS = 20;
+export const COL_WIDTH_CHART_PADDING = 10;
+
+// Resource name display
+export const MAX_FILENAME_LENGTH = 23;
+export const TRUNCATED_FILENAME_LENGTH = 20;
+
+// Waterfall chart
+export const MIN_DURATION_THRESHOLD = 0.1;
diff --git a/packages/cookiebench-cli/src/utils/index.ts b/packages/cookiebench-cli/src/utils/index.ts
new file mode 100644
index 0000000..1126cd5
--- /dev/null
+++ b/packages/cookiebench-cli/src/utils/index.ts
@@ -0,0 +1,24 @@
+/** biome-ignore-all lint/performance/noBarrelFile: this is a barrel file */
+import { readConfig as readConfigShared } from "@consentio/shared";
+import type { Config } from "../types";
+
+// Re-export shared constants
+// Re-export shared utilities
+export {
+ formatBytes,
+ formatTime,
+ getPackageManager,
+ HALF_SECOND,
+ KILOBYTE,
+ ONE_SECOND,
+ PERCENTAGE_DIVISOR,
+ PERCENTAGE_MULTIPLIER,
+} from "@consentio/shared";
+// Re-export local constants
+
+export * from "./constants";
+export { findProjectRoot, resolveBenchmarkPath } from "./project-root";
+
+export function readConfig(configPath?: string): Config | null {
+ return readConfigShared(configPath);
+}
diff --git a/packages/cookiebench-cli/src/utils/logger.ts b/packages/cookiebench-cli/src/utils/logger.ts
new file mode 100644
index 0000000..9c83c7d
--- /dev/null
+++ b/packages/cookiebench-cli/src/utils/logger.ts
@@ -0,0 +1,162 @@
+import { createLogger, type Logger } from "@c15t/logger";
+import { log, note, outro } from "@clack/prompts";
+import { inspect } from "node:util";
+import color from "picocolors";
+
+// Define standard log levels
+export type LogLevel = "error" | "warn" | "info" | "debug";
+export const validLogLevels: LogLevel[] = ["error", "warn", "info", "debug"];
+export type CliLogger = Logger & CliExtensions;
+
+// Define CLI-specific extension levels with their method signatures
+export type CliExtensions = {
+ message: (message: string, ...args: unknown[]) => void;
+ note: (message: string, title?: string) => void;
+ outro: (message: string) => void;
+ step: (message: string) => void;
+ clear: () => void;
+};
+
+const formatArgs = (args: unknown[]): string => {
+ if (args.length === 0) {
+ return "";
+ }
+ return `\n${args
+ .map((arg) => {
+ try {
+ return ` - ${JSON.stringify(arg, null, 2)}`;
+ } catch {
+ // Fallback to util.inspect for circular references or other serialization errors
+ return ` - ${inspect(arg, { depth: null })}`;
+ }
+ })
+ .join("\n")}`;
+};
+
+/**
+ * Formats a log message with appropriate styling based on log level
+ *
+ * @param logLevel - The log level to format for
+ * @param message - The message to format
+ * @param args - Additional arguments to format
+ * @returns The formatted message string
+ */
+export const formatLogMessage = (
+ logLevel: LogLevel | string,
+ message: unknown,
+ args: unknown[] = []
+): string => {
+ const messageStr = typeof message === "string" ? message : String(message);
+ const formattedArgs = formatArgs(args);
+
+ switch (logLevel) {
+ case "error": {
+ return `${color.bgRed(color.black(" error "))} ${messageStr}${formattedArgs}`;
+ }
+ case "warn": {
+ return `${color.bgYellow(color.black(" warning "))} ${messageStr}${formattedArgs}`;
+ }
+ case "info": {
+ return `${color.bgCyan(color.black(" info "))} ${messageStr}${formattedArgs}`;
+ }
+ case "debug": {
+ return `${color.bgBlack(color.white(" debug "))} ${messageStr}${formattedArgs}`;
+ }
+ case "success": {
+ return `${color.bgGreen(color.white(" success "))} ${messageStr}${formattedArgs}`;
+ }
+ default: {
+ // Handle unexpected levels
+ const levelStr = String(logLevel);
+ return `[${levelStr.toUpperCase()}] ${messageStr}${formattedArgs}`;
+ }
+ }
+};
+
+/**
+ * Logs a message with the appropriate clack prompt styling
+ * Can be used before logger initialization
+ *
+ * @param logLevel - The log level to use
+ * @param message - The message to log
+ * @param args - Additional arguments to include
+ */
+export const logMessage = (
+ logLevel: LogLevel | "success" | string,
+ message: unknown,
+ ...args: unknown[]
+): void => {
+ const formattedMessage = formatLogMessage(logLevel, message, args);
+
+ switch (logLevel) {
+ case "error":
+ log.error(formattedMessage);
+ break;
+ case "warn":
+ log.warn(formattedMessage);
+ break;
+ case "info":
+ case "debug":
+ log.info(formattedMessage);
+ break;
+ case "success":
+ log.success(formattedMessage);
+ break;
+ default:
+ log.message(formattedMessage);
+ }
+};
+
+// This function creates a logger instance based on the provided level
+// It includes the custom log handler for clack integration.
+export const createCliLogger = (level: LogLevel = "info"): CliLogger => {
+ // Create the base logger with standard levels
+ const baseLogger = createLogger({
+ level,
+ appName: "cookiebench",
+ log: (
+ logLevel: LogLevel | "success",
+ message: string,
+ ...args: unknown[]
+ ) => {
+ // Level filtering is primarily handled by the createLogger factory's level setting.
+ // This function now just focuses on routing output.
+ logMessage(logLevel, message, ...args);
+ },
+ });
+
+ // Extend the logger with CLI-specific methods
+ const extendedLogger = baseLogger as CliLogger;
+
+ // Add message method (plain text without prefix)
+ extendedLogger.message = (message: string) => {
+ log.message(message);
+ };
+
+ // Add note method (creates a note box)
+ extendedLogger.note = (message: string, title?: string) => {
+ const messageStr = typeof message === "string" ? message : String(message);
+ note(messageStr, title);
+ };
+
+ // Add step method
+ extendedLogger.step = (message: string) => {
+ log.step(message);
+ };
+
+ // Add outro method (uses plain message)
+ extendedLogger.outro = (message: string) => {
+ outro(message);
+ };
+
+ // Add clear method
+ extendedLogger.clear = () => {
+ // biome-ignore lint/suspicious/noConsole: this is a CLI tool
+ console.clear();
+ };
+
+ return extendedLogger;
+};
+
+// Export a default logger instance
+export const logger = createCliLogger();
diff --git a/packages/cookiebench-cli/src/utils/project-root.ts b/packages/cookiebench-cli/src/utils/project-root.ts
new file mode 100644
index 0000000..49ef2bd
--- /dev/null
+++ b/packages/cookiebench-cli/src/utils/project-root.ts
@@ -0,0 +1,36 @@
+import { existsSync } from "node:fs";
+import { dirname, isAbsolute, join } from "node:path";
+
+export function findProjectRoot(startDir = process.cwd()): string {
+ let currentDir = startDir;
+
+ while (currentDir !== dirname(currentDir)) {
+ const hasWorkspaceFile = existsSync(join(currentDir, "pnpm-workspace.yaml"));
+ const hasBenchmarksDir = existsSync(join(currentDir, "benchmarks"));
+ const hasPackagesDir = existsSync(join(currentDir, "packages"));
+
+ if (hasWorkspaceFile && hasBenchmarksDir && hasPackagesDir) {
+ return currentDir;
+ }
+
+ currentDir = dirname(currentDir);
+ }
+
+ return startDir;
+}
+
+export function resolveBenchmarkPath(
+ projectRoot: string,
+ appPath: string
+): string {
+ if (isAbsolute(appPath)) {
+ return appPath;
+ }
+
+ const directPath = join(projectRoot, appPath);
+ if (existsSync(join(directPath, "config.json"))) {
+ return directPath;
+ }
+
+ return join(projectRoot, "benchmarks", appPath);
+}
diff --git a/packages/cookiebench-cli/src/utils/scoring.ts b/packages/cookiebench-cli/src/utils/scoring.ts
new file mode 100644
index 0000000..510daad
--- /dev/null
+++ b/packages/cookiebench-cli/src/utils/scoring.ts
@@ -0,0 +1,1445 @@
+/** biome-ignore-all lint/style/noMagicNumbers: scoring requires many numeric thresholds */
+/** biome-ignore-all lint/complexity/noExcessiveCognitiveComplexity: scoring logic is complex */
+/** biome-ignore-all lint/style/noNestedTernary: scoring logic uses ternary expressions */
+import Table from "cli-table3";
+import type { BenchmarkScores } from "../types";
+import { formatBytes, formatTime } from "./index";
+
+// Type definitions for better type safety
+type AppData = {
+ id?: number;
+ name: string;
+ baseline: boolean;
+ company: string | null;
+ techStack: string;
+ source: string | null;
+ tags: string | null;
+};
+
+type MetricsData = {
+ fcp: number;
+ lcp: number;
+ cls: number;
+ tti: number;
+ tbt: number;
+ totalSize: number;
+ thirdPartySize: number;
+ bannerVisibilityTime: number;
+ viewportCoverage: number;
+ resourceCount?: number;
+ scriptLoadTime?: number;
+ isBundled?: boolean;
+ isIIFE?: boolean;
+ // NEW: Perfume.js enhanced metrics
+ timeToFirstByte?: number;
+ interactionToNextPaint?: number | null;
+ networkInformation?: {
+ effectiveType: string;
+ downlink: number;
+ rtt: number;
+ saveData: boolean;
+ };
+};
+
+type ResourceData = {
+ size: number;
+ isThirdParty: boolean;
+};
+
+type BenchmarkData = {
+ bannerRenderTime?: number;
+ bannerInteractionTime?: number;
+ layoutShift?: number;
+};
+
+export type TechStackData = {
+ languages: string[];
+ frameworks: string[];
+ bundler: string;
+ bundleType: string;
+ packageManager: string;
+ typescript: boolean;
+};
+
+type CompanyData = {
+ name: string;
+ avatar: string;
+};
+
+type SourceData = {
+ license?: string;
+ github?: string;
+ repository?: string;
+ openSource?: boolean;
+ type?: string;
+};
+
+type CategoryScores = {
+ performance: number;
+ bundleStrategy: number;
+ networkImpact: number;
+ transparency: number;
+ userExperience: number;
+};
+
+type ScoreDetail = {
+ metric: string;
+ value: string | number;
+ score: number;
+ maxScore: number;
+ reason: string;
+};
+
+type ScoreWeights = {
+ performance: number;
+ bundleStrategy: number;
+ networkImpact: number;
+ transparency: number;
+ userExperience: number;
+};
+
+// Default scoring weights
+export const DEFAULT_SCORE_WEIGHTS: ScoreWeights = {
+ performance: 0.4, // 40% - Core Web Vitals and performance metrics
+ bundleStrategy: 0.25, // 25% - First-party vs third-party, bundling approach
+ networkImpact: 0.2, // 20% - Network requests, bundle size, third-party impact
+ transparency: 0.1, // 10% - Open source, company info, tech stack disclosure
+ userExperience: 0.05, // 5% - Layout stability, interaction responsiveness
+};
+
+// Helper function to determine if a solution is open source
+function isOpenSourceSolution(
+ app: AppData,
+ sourceInfo: SourceData | null,
+ tags: string
+): boolean {
+ // Check source information for open source indicators
+ if (sourceInfo) {
+ // Check for open source license
+ const license = sourceInfo.license?.toLowerCase() || "";
+ const openSourceLicenses = [
+ "mit",
+ "apache",
+ "gpl",
+ "bsd",
+ "lgpl",
+ "mpl",
+ "isc",
+ "unlicense",
+ "cc0",
+ "wtfpl",
+ "zlib",
+ "artistic",
+ "epl",
+ "cddl",
+ ];
+
+ if (openSourceLicenses.some((lic) => license.includes(lic))) {
+ return true;
+ }
+
+ // Check GitHub repository
+ if (sourceInfo.github || sourceInfo.repository?.includes("github.com")) {
+ return true;
+ }
+
+ // Check if explicitly marked as open source
+ if (sourceInfo.openSource === true || sourceInfo.type === "open-source") {
+ return true;
+ }
+ }
+
+ // Check tags for open source indicators
+ const lowerTags = tags.toLowerCase();
+ if (
+ lowerTags.includes("open source") ||
+ lowerTags.includes("opensource") ||
+ lowerTags.includes("oss") ||
+ lowerTags.includes("free") ||
+ lowerTags.includes("community")
+ ) {
+ return true;
+ }
+
+ // Check app name for known open source solutions
+ const appName = app.name.toLowerCase();
+ const knownOpenSource = [
+ "c15t",
+ "cookieconsent",
+ "klaro",
+ "tarteaucitron",
+ "osano",
+ "react-cookie-consent",
+ "vanilla-cookieconsent",
+ "baseline",
+ ];
+
+ if (knownOpenSource.some((name) => appName.includes(name))) {
+ return true;
+ }
+
+ return false;
+}
+
+// Helper function to parse tech stack
+function parseTechStack(techStackJson: string): TechStackData {
+ try {
+ const techStack = JSON.parse(techStackJson);
+ return {
+ languages: techStack.languages || [],
+ frameworks: techStack.frameworks || [],
+ bundler: techStack.bundler || "unknown",
+ bundleType: techStack.bundleType || "unknown",
+ packageManager: techStack.packageManager || "unknown",
+ typescript: techStack.typescript,
+ };
+ } catch {
+ return {
+ languages: [],
+ frameworks: [],
+ bundler: "unknown",
+ bundleType: "unknown",
+ packageManager: "unknown",
+ typescript: false,
+ };
+ }
+}
+
+// Helper function to parse company info
+function parseCompany(companyJson: string | null): CompanyData | null {
+ if (!companyJson) {
+ return null;
+ }
+ try {
+ return JSON.parse(companyJson);
+ } catch {
+ return null;
+ }
+}
+
+// Helper function to parse source info
+function parseSource(sourceJson: string | null): SourceData | null {
+ if (!sourceJson) {
+ return null;
+ }
+ try {
+ return JSON.parse(sourceJson);
+ } catch {
+ return null;
+ }
+}
+
+// Calculate performance score (out of 100) with more sensitive thresholds
+function calculatePerformanceScore(metrics: MetricsData): {
+ score: number;
+ maxScore: number;
+ details: ScoreDetail[];
+} {
+ const details: ScoreDetail[] = [];
+ let totalScore = 0;
+ const maxScore = 100;
+
+ // Ensure all metrics are finite numbers
+ const fcp = Number.isFinite(metrics.fcp) ? metrics.fcp : 0;
+ const lcp = Number.isFinite(metrics.lcp) ? metrics.lcp : 0;
+ const cls = Number.isFinite(metrics.cls) ? metrics.cls : 0;
+ const tti = Number.isFinite(metrics.tti) ? metrics.tti : 0;
+ const tbt = Number.isFinite(metrics.tbt) ? metrics.tbt : 0;
+
+ // FCP Score (15 points) - More sensitive for fast sites
+ const fcpScore =
+ fcp <= 50 ? 15 : fcp <= 100 ? 13 : fcp <= 200 ? 10 : fcp <= 500 ? 7 : 3;
+ totalScore += fcpScore;
+ details.push({
+ metric: "First Contentful Paint",
+ value: formatTime(fcp),
+ score: fcpScore,
+ maxScore: 15,
+ reason:
+ fcp <= 50
+ ? "Excellent"
+ : fcp <= 100
+ ? "Very Good"
+ : fcp <= 200
+ ? "Good"
+ : fcp <= 500
+ ? "Fair"
+ : "Poor",
+ });
+
+ // LCP Score (20 points) - More sensitive for banner rendering
+ const lcpScore =
+ lcp <= 100 ? 20 : lcp <= 300 ? 16 : lcp <= 500 ? 12 : lcp <= 1000 ? 8 : 4;
+ totalScore += lcpScore;
+ details.push({
+ metric: "Largest Contentful Paint",
+ value: formatTime(lcp),
+ score: lcpScore,
+ maxScore: 20,
+ reason:
+ lcp <= 100
+ ? "Excellent"
+ : lcp <= 300
+ ? "Very Good"
+ : lcp <= 500
+ ? "Good"
+ : lcp <= 1000
+ ? "Fair"
+ : "Poor",
+ });
+
+ // CLS Score (20 points)
+ const clsScore =
+ cls <= 0.01 ? 20 : cls <= 0.05 ? 15 : cls <= 0.1 ? 10 : cls <= 0.25 ? 5 : 0;
+ totalScore += clsScore;
+ details.push({
+ metric: "Cumulative Layout Shift",
+ value: cls.toFixed(3),
+ score: clsScore,
+ maxScore: 20,
+ reason:
+ cls <= 0.01
+ ? "Excellent"
+ : cls <= 0.05
+ ? "Very Good"
+ : cls <= 0.1
+ ? "Good"
+ : cls <= 0.25
+ ? "Fair"
+ : "Poor",
+ });
+
+ // TTI Score (15 points) - Cookie banners should be interactive quickly
+ const ttiScore =
+ tti <= 1000 ? 15 : tti <= 1500 ? 12 : tti <= 2000 ? 8 : tti <= 3000 ? 4 : 0;
+ totalScore += ttiScore;
+ details.push({
+ metric: "Time to Interactive",
+ value: formatTime(tti),
+ score: ttiScore,
+ maxScore: 15,
+ reason:
+ tti <= 1000
+ ? "Excellent"
+ : tti <= 1500
+ ? "Very Good"
+ : tti <= 2000
+ ? "Good"
+ : tti <= 3000
+ ? "Fair"
+ : "Poor",
+ });
+
+ // TBT Score (10 points) - Main thread blocking
+ const tbtScore = tbt <= 50 ? 10 : tbt <= 200 ? 7 : tbt <= 500 ? 3 : 0;
+ totalScore += tbtScore;
+ details.push({
+ metric: "Total Blocking Time",
+ value: formatTime(tbt),
+ score: tbtScore,
+ maxScore: 10,
+ reason:
+ tbt <= 50
+ ? "Excellent"
+ : tbt <= 200
+ ? "Good"
+ : tbt <= 500
+ ? "Fair"
+ : "Poor",
+ });
+
+ // TTFB Score (10 points) - Server response time
+ const ttfb =
+ Number.isFinite(metrics.timeToFirstByte) &&
+ metrics.timeToFirstByte !== undefined
+ ? metrics.timeToFirstByte
+ : 0;
+ const ttfbScore =
+ ttfb <= 100 ? 10 : ttfb <= 200 ? 8 : ttfb <= 400 ? 5 : ttfb <= 600 ? 3 : 0;
+ totalScore += ttfbScore;
+ details.push({
+ metric: "Time to First Byte",
+ value: formatTime(ttfb),
+ score: ttfbScore,
+ maxScore: 10,
+ reason:
+ ttfb <= 100
+ ? "Excellent"
+ : ttfb <= 200
+ ? "Good"
+ : ttfb <= 400
+ ? "Fair"
+ : ttfb <= 600
+ ? "Poor"
+ : "Very Poor",
+ });
+
+ // INP Score (10 points) - Interaction responsiveness (replaces/complements FID)
+ const inp =
+ Number.isFinite(metrics.interactionToNextPaint) &&
+ metrics.interactionToNextPaint !== undefined &&
+ metrics.interactionToNextPaint !== null
+ ? metrics.interactionToNextPaint
+ : null;
+ const inpScore =
+ inp === null ? 5 : inp <= 200 ? 10 : inp <= 500 ? 7 : inp <= 1000 ? 4 : 0;
+ totalScore += inpScore;
+ details.push({
+ metric: "Interaction to Next Paint",
+ value: inp === null ? "N/A" : formatTime(inp),
+ score: inpScore,
+ maxScore: 10,
+ reason:
+ inp === null
+ ? "No interactions detected"
+ : inp <= 200
+ ? "Excellent"
+ : inp <= 500
+ ? "Good"
+ : inp <= 1000
+ ? "Fair"
+ : "Poor",
+ });
+
+ return { score: totalScore, maxScore, details };
+}
+
+// Calculate bundle strategy score (out of 100)
+function calculateBundleScore(
+ metrics: MetricsData,
+ techStack: TechStackData,
+ resourceData: ResourceData[]
+): {
+ score: number;
+ maxScore: number;
+ details: ScoreDetail[];
+} {
+ const details: ScoreDetail[] = [];
+ let totalScore = 0;
+ const maxScore = 100;
+
+ // Bundle strategy (40 points)
+ const bundleScore = metrics.isBundled ? 40 : metrics.isIIFE ? 20 : 10;
+ totalScore += bundleScore;
+ details.push({
+ metric: "Bundle Strategy",
+ value: metrics.isBundled ? "Bundled" : metrics.isIIFE ? "IIFE" : "Unknown",
+ score: bundleScore,
+ maxScore: 40,
+ reason: metrics.isBundled
+ ? "First-party bundled"
+ : metrics.isIIFE
+ ? "External script"
+ : "Unknown strategy",
+ });
+
+ // Third-party dependency ratio (30 points)
+ const thirdPartyResources = resourceData.filter((r) => r.isThirdParty);
+ const thirdPartyRatio =
+ thirdPartyResources.length / Math.max(resourceData.length, 1);
+ const thirdPartyScore =
+ thirdPartyRatio <= 0.1
+ ? 30
+ : thirdPartyRatio <= 0.3
+ ? 20
+ : thirdPartyRatio <= 0.5
+ ? 10
+ : 0;
+ totalScore += thirdPartyScore;
+ details.push({
+ metric: "Third-party Dependencies",
+ value: `${thirdPartyResources.length}/${resourceData.length}`,
+ score: thirdPartyScore,
+ maxScore: 30,
+ reason:
+ thirdPartyRatio <= 0.1
+ ? "Minimal third-party"
+ : thirdPartyRatio <= 0.3
+ ? "Low third-party"
+ : thirdPartyRatio <= 0.5
+ ? "Moderate third-party"
+ : "Heavy third-party",
+ });
+
+ // Modern bundler (20 points)
+ const modernBundlers = [
+ "webpack",
+ "vite",
+ "rollup",
+ "esbuild",
+ "turbopack",
+ "rspack",
+ "rslib",
+ "nextjs",
+ ];
+ const bundlerScore =
+ techStack && modernBundlers.includes(techStack.bundler.toLowerCase())
+ ? 20
+ : 10;
+ totalScore += bundlerScore;
+ details.push({
+ metric: "Bundler",
+ value: techStack?.bundler || "Unknown",
+ score: bundlerScore,
+ maxScore: 20,
+ reason:
+ techStack && modernBundlers.includes(techStack.bundler.toLowerCase())
+ ? "Modern bundler"
+ : "Legacy/unknown bundler",
+ });
+
+ // TypeScript usage (10 points)
+ const tsScore = techStack?.typescript ? 10 : 0;
+ totalScore += tsScore;
+ details.push({
+ metric: "TypeScript",
+ value: techStack?.typescript ? "Yes" : "No",
+ score: tsScore,
+ maxScore: 10,
+ reason: techStack?.typescript ? "Type safety" : "No type safety",
+ });
+
+ return { score: totalScore, maxScore, details };
+}
+
+// Calculate network impact score (out of 100)
+function calculateNetworkScore(
+ metrics: MetricsData,
+ _resourceData: ResourceData[]
+): {
+ score: number;
+ maxScore: number;
+ details: ScoreDetail[];
+} {
+ const details: ScoreDetail[] = [];
+ let totalScore = 0;
+ const maxScore = 100;
+
+ // Ensure metrics are finite numbers
+ const totalSize = Number.isFinite(metrics.totalSize) ? metrics.totalSize : 0;
+ const thirdPartySize = Number.isFinite(metrics.thirdPartySize)
+ ? metrics.thirdPartySize
+ : 0;
+ const resourceCount = Number.isFinite(metrics.resourceCount)
+ ? metrics.resourceCount || 0
+ : 0;
+ const scriptLoadTime = Number.isFinite(metrics.scriptLoadTime)
+ ? metrics.scriptLoadTime || 0
+ : 0;
+
+ // Total size impact (35 points) - More sensitive for lightweight solutions
+ const totalSizeKB = totalSize / 1024;
+ const sizeScore =
+ totalSizeKB <= 50
+ ? 35
+ : totalSizeKB <= 100
+ ? 25
+ : totalSizeKB <= 200
+ ? 15
+ : totalSizeKB <= 500
+ ? 10
+ : 5;
+ totalScore += sizeScore;
+ details.push({
+ metric: "Total Bundle Size",
+ value: formatBytes(totalSize),
+ score: sizeScore,
+ maxScore: 35,
+ reason:
+ totalSizeKB <= 50
+ ? "Ultra lightweight"
+ : totalSizeKB <= 100
+ ? "Lightweight"
+ : totalSizeKB <= 200
+ ? "Moderate"
+ : totalSizeKB <= 500
+ ? "Heavy"
+ : "Very heavy",
+ });
+
+ // Third-party size impact (25 points)
+ const thirdPartySizeKB = thirdPartySize / 1024;
+ const thirdPartyNetworkScore =
+ thirdPartySizeKB === 0
+ ? 25
+ : thirdPartySizeKB <= 50
+ ? 15
+ : thirdPartySizeKB <= 100
+ ? 10
+ : 5;
+ totalScore += thirdPartyNetworkScore;
+ details.push({
+ metric: "Third-party Size",
+ value: formatBytes(thirdPartySize),
+ score: thirdPartyNetworkScore,
+ maxScore: 25,
+ reason:
+ thirdPartySizeKB === 0
+ ? "Zero third-party"
+ : thirdPartySizeKB <= 50
+ ? "Minimal third-party"
+ : thirdPartySizeKB <= 100
+ ? "Moderate third-party"
+ : "Heavy third-party",
+ });
+
+ // Network requests (25 points)
+ const requestScore =
+ resourceCount <= 3
+ ? 25
+ : resourceCount <= 5
+ ? 20
+ : resourceCount <= 10
+ ? 15
+ : resourceCount <= 15
+ ? 10
+ : 5;
+ totalScore += requestScore;
+ details.push({
+ metric: "Network Requests",
+ value: resourceCount.toString(),
+ score: requestScore,
+ maxScore: 25,
+ reason:
+ resourceCount <= 3
+ ? "Minimal requests"
+ : resourceCount <= 5
+ ? "Low requests"
+ : resourceCount <= 10
+ ? "Moderate requests"
+ : resourceCount <= 15
+ ? "Many requests"
+ : "Too many requests",
+ });
+
+ // Script load time (15 points)
+ const scriptScore =
+ scriptLoadTime <= 50
+ ? 15
+ : scriptLoadTime <= 100
+ ? 10
+ : scriptLoadTime <= 200
+ ? 5
+ : 0;
+ totalScore += scriptScore;
+ details.push({
+ metric: "Script Load Time",
+ value: formatTime(scriptLoadTime),
+ score: scriptScore,
+ maxScore: 15,
+ reason:
+ scriptLoadTime <= 50
+ ? "Very fast loading"
+ : scriptLoadTime <= 100
+ ? "Fast loading"
+ : scriptLoadTime <= 200
+ ? "Moderate loading"
+ : "Slow loading",
+ });
+
+ return { score: totalScore, maxScore, details };
+}
+
+// Calculate transparency score (out of 100)
+function calculateTransparencyScore(
+ isOpenSource: boolean,
+ company: CompanyData | null,
+ techStack: TechStackData
+): {
+ score: number;
+ maxScore: number;
+ details: ScoreDetail[];
+} {
+ const details: ScoreDetail[] = [];
+ let totalScore = 0;
+ const maxScore = 100;
+
+ // Open source bonus (60 points)
+ const openSourceScore = isOpenSource ? 60 : 0;
+ totalScore += openSourceScore;
+ details.push({
+ metric: "Open Source",
+ value: isOpenSource ? "Yes" : "No",
+ score: openSourceScore,
+ maxScore: 60,
+ reason: isOpenSource ? "Transparent & auditable" : "Proprietary solution",
+ });
+
+ // Company transparency (25 points)
+ const companyScore = company ? 25 : 10;
+ totalScore += companyScore;
+ details.push({
+ metric: "Company Info",
+ value: company ? "Available" : "Limited",
+ score: companyScore,
+ maxScore: 25,
+ reason: company ? "Clear attribution" : "Limited transparency",
+ });
+
+ // Tech stack disclosure (15 points)
+ const techScore = techStack && techStack.bundler !== "unknown" ? 15 : 5;
+ totalScore += techScore;
+ details.push({
+ metric: "Tech Stack",
+ value: techStack ? "Disclosed" : "Unknown",
+ score: techScore,
+ maxScore: 15,
+ reason:
+ techStack && techStack.bundler !== "unknown"
+ ? "Technical transparency"
+ : "Limited tech info",
+ });
+
+ return { score: totalScore, maxScore, details };
+}
+
+// Calculate user experience score (out of 100)
+function calculateUXScore(
+ metrics: MetricsData,
+ benchmarkData: BenchmarkData
+): {
+ score: number;
+ maxScore: number;
+ details: ScoreDetail[];
+} {
+ const details: ScoreDetail[] = [];
+ let totalScore = 0;
+ const maxScore = 100;
+
+ // Layout stability (40 points)
+ const cls = Number.isFinite(metrics.cls) ? metrics.cls : 0;
+ const clsScore =
+ cls <= 0.01
+ ? 40
+ : cls <= 0.05
+ ? 30
+ : cls <= 0.1
+ ? 20
+ : cls <= 0.25
+ ? 10
+ : 0;
+ totalScore += clsScore;
+ details.push({
+ metric: "Layout Stability",
+ value: cls.toFixed(3),
+ score: clsScore,
+ maxScore: 40,
+ reason:
+ cls <= 0.01
+ ? "No layout shifts"
+ : cls <= 0.05
+ ? "Minimal shifts"
+ : cls <= 0.1
+ ? "Minor shifts"
+ : cls <= 0.25
+ ? "Some shifts"
+ : "Significant shifts",
+ });
+
+ // Banner visible time (35 points) — scored from user-visible time only, not DOM presence
+ const bannerVisibleTimeMs =
+ Number.isFinite(benchmarkData.bannerRenderTime)
+ ? benchmarkData.bannerRenderTime || 0
+ : metrics.bannerVisibilityTime || 0;
+ const renderScore =
+ bannerVisibleTimeMs <= 25
+ ? 35
+ : bannerVisibleTimeMs <= 50
+ ? 25
+ : bannerVisibleTimeMs <= 100
+ ? 15
+ : bannerVisibleTimeMs <= 200
+ ? 10
+ : 5;
+ totalScore += renderScore;
+ details.push({
+ metric: "Banner visible time",
+ value: formatTime(bannerVisibleTimeMs),
+ score: renderScore,
+ maxScore: 35,
+ reason:
+ bannerVisibleTimeMs <= 25
+ ? "Instant visibility"
+ : bannerVisibleTimeMs <= 50
+ ? "Very fast visibility"
+ : bannerVisibleTimeMs <= 100
+ ? "Fast visibility"
+ : bannerVisibleTimeMs <= 200
+ ? "Moderate visibility"
+ : "Slow visibility",
+ });
+
+ // Viewport coverage impact (25 points)
+ const coverage = Number.isFinite(metrics.viewportCoverage)
+ ? metrics.viewportCoverage
+ : 0;
+ const coverageScore =
+ coverage <= 10
+ ? 25
+ : coverage <= 20
+ ? 20
+ : coverage <= 30
+ ? 15
+ : coverage <= 50
+ ? 10
+ : 5;
+ totalScore += coverageScore;
+ details.push({
+ metric: "Viewport Coverage",
+ value: `${coverage.toFixed(1)}%`,
+ score: coverageScore,
+ maxScore: 25,
+ reason:
+ coverage <= 10
+ ? "Minimal intrusion"
+ : coverage <= 20
+ ? "Low intrusion"
+ : coverage <= 30
+ ? "Moderate intrusion"
+ : coverage <= 50
+ ? "High intrusion"
+ : "Very intrusive",
+ });
+
+ return { score: totalScore, maxScore, details };
+}
+
+// Generate insights based on scores
+function generateInsights(
+ categoryScores: CategoryScores,
+ metrics: MetricsData,
+ resourceData: ResourceData[],
+ isOpenSource: boolean
+): string[] {
+ const insights: string[] = [];
+
+ // Performance insights
+ if (categoryScores.performance >= 90) {
+ insights.push(
+ "Outstanding performance metrics across all Core Web Vitals."
+ );
+ } else if (categoryScores.performance < 60) {
+ insights.push(
+ "Performance optimization needed - focus on reducing load times and layout shifts."
+ );
+ }
+
+ // Bundle strategy insights
+ if (metrics.isBundled) {
+ insights.push(
+ "Excellent bundle strategy - first-party bundling reduces network overhead and improves reliability."
+ );
+ } else if (metrics.isIIFE) {
+ insights.push(
+ "Consider bundling strategy to reduce third-party dependencies and improve performance."
+ );
+ }
+
+ // Open source insights
+ if (isOpenSource) {
+ insights.push(
+ "Open source solution provides transparency and community-driven development."
+ );
+ } else {
+ insights.push(
+ "Consider open source alternatives for better transparency and community support."
+ );
+ }
+
+ // Network insights
+ const thirdPartyResources = resourceData.filter((r) => r.isThirdParty);
+ if (thirdPartyResources.length === 0) {
+ insights.push(
+ "Zero third-party dependencies minimize privacy concerns and improve reliability."
+ );
+ } else if (thirdPartyResources.length > 5) {
+ insights.push(
+ "High number of third-party requests may impact performance and privacy."
+ );
+ }
+
+ // Network quality context (from Perfume.js)
+ if (metrics.networkInformation) {
+ const netInfo = metrics.networkInformation;
+ insights.push(
+ `Tested on ${netInfo.effectiveType} connection (${netInfo.downlink} Mbps, ${netInfo.rtt}ms RTT).`
+ );
+ }
+
+ // TTFB insights
+ if (metrics.timeToFirstByte) {
+ if (metrics.timeToFirstByte <= 100) {
+ insights.push(
+ "Excellent server response time (TTFB) ensures fast initial loading."
+ );
+ } else if (metrics.timeToFirstByte > 600) {
+ insights.push(
+ "Server response time (TTFB) is slow - consider CDN or server optimization."
+ );
+ }
+ }
+
+ // INP insights (if interactions detected)
+ if (
+ metrics.interactionToNextPaint !== null &&
+ metrics.interactionToNextPaint !== undefined &&
+ metrics.interactionToNextPaint > 500
+ ) {
+ insights.push(
+ "Interaction responsiveness (INP) needs improvement for better user experience."
+ );
+ }
+
+ return insights;
+}
+
+// Generate recommendations based on scores
+function generateRecommendations(
+ categoryScores: CategoryScores,
+ metrics: MetricsData,
+ resourceData: ResourceData[]
+): string[] {
+ const recommendations: string[] = [];
+
+ // Performance recommendations
+ if (categoryScores.performance < 80) {
+ if (metrics.fcp > 100) {
+ recommendations.push(
+ "Optimize First Contentful Paint by reducing render-blocking resources."
+ );
+ }
+ if (metrics.lcp > 300) {
+ recommendations.push(
+ "Improve Largest Contentful Paint by optimizing critical resource loading."
+ );
+ }
+ if (metrics.cls > 0.05) {
+ recommendations.push(
+ "Reduce Cumulative Layout Shift by reserving space for dynamic content."
+ );
+ }
+ if (metrics.tbt > 50) {
+ recommendations.push(
+ "Reduce Total Blocking Time by optimizing JavaScript execution."
+ );
+ }
+ }
+
+ // TTFB and INP recommendations
+ if (metrics.timeToFirstByte && metrics.timeToFirstByte > 200) {
+ recommendations.push(
+ "Improve Time to First Byte (TTFB) through server optimization, CDN usage, or caching strategies."
+ );
+ }
+ if (metrics.interactionToNextPaint && metrics.interactionToNextPaint > 200) {
+ recommendations.push(
+ "Optimize Interaction to Next Paint (INP) by reducing JavaScript execution time and improving event handler performance."
+ );
+ }
+
+ // Bundle strategy recommendations
+ if (categoryScores.bundleStrategy < 70) {
+ if (!metrics.isBundled) {
+ recommendations.push(
+ "Consider bundling cookie consent code with your main application bundle."
+ );
+ }
+ const thirdPartyRatio =
+ resourceData.filter((r) => r.isThirdParty).length /
+ Math.max(resourceData.length, 1);
+ if (thirdPartyRatio > 0.3) {
+ recommendations.push(
+ "Reduce third-party dependencies to improve reliability and performance."
+ );
+ }
+ }
+
+ // Network recommendations
+ if (categoryScores.networkImpact < 70) {
+ if (metrics.totalSize > 100 * 1024) {
+ recommendations.push(
+ "Reduce bundle size through code splitting and tree shaking."
+ );
+ }
+ if (metrics.thirdPartySize > 0) {
+ recommendations.push(
+ "Eliminate or reduce third-party resources for better performance."
+ );
+ }
+ }
+
+ return recommendations;
+}
+
+// Get score grade based on total score
+function getScoreGrade(score: number): BenchmarkScores["grade"] {
+ if (score >= 90) {
+ return "Excellent";
+ }
+ if (score >= 80) {
+ return "Good";
+ }
+ if (score >= 70) {
+ return "Fair";
+ }
+ if (score >= 60) {
+ return "Poor";
+ }
+ return "Critical";
+}
+
+// Get category status based on score percentage
+function getCategoryStatus(
+ score: number,
+ maxScore: number
+): "excellent" | "good" | "fair" | "poor" {
+ const percentage = (score / maxScore) * 100;
+ if (percentage >= 90) {
+ return "excellent";
+ }
+ if (percentage >= 75) {
+ return "good";
+ }
+ if (percentage >= 60) {
+ return "fair";
+ }
+ return "poor";
+}
+
+// Main scoring function with CLI-compatible interface
+// biome-ignore lint/nursery/useMaxParams: legacy API with multiple metric groups
+export function calculateScores(
+ metrics: {
+ fcp: number;
+ lcp: number;
+ cls: number;
+ tbt: number;
+ tti: number;
+ timeToFirstByte?: number;
+ interactionToNextPaint?: number | null;
+ },
+ bundleMetrics: {
+ totalSize: number;
+ jsSize: number;
+ cssSize: number;
+ imageSize: number;
+ fontSize: number;
+ otherSize: number;
+ },
+ networkMetrics: {
+ totalRequests: number;
+ thirdPartyRequests: number;
+ thirdPartySize: number;
+ thirdPartyDomains: number;
+ },
+ /** Must use user-visible time (opacity > 0.5), not DOM presence time, for scoring. */
+ transparencyMetrics: {
+ cookieBannerDetected: boolean;
+ /** User-visible time (ms). Do not pass DOM presence time here. */
+ cookieBannerVisibleTimeMs: number | null;
+ cookieBannerCoverage: number;
+ },
+ userExperienceMetrics: {
+ domSize: number;
+ mainThreadBlocking: number;
+ layoutShifts: number;
+ },
+ isBaseline = false,
+ appData?: AppData,
+ networkInformation?: {
+ effectiveType: string;
+ downlink: number;
+ rtt: number;
+ saveData: boolean;
+ }
+): BenchmarkScores {
+ if (isBaseline) {
+ return {
+ totalScore: 100,
+ grade: "Excellent",
+ categoryScores: {
+ performance: 100,
+ bundleStrategy: 100,
+ networkImpact: 100,
+ transparency: 100,
+ userExperience: 100,
+ },
+ categories: [
+ {
+ name: "Performance",
+ score: 100,
+ maxScore: 100,
+ weight: 1,
+ details: [
+ {
+ name: "Core Web Vitals",
+ score: 100,
+ maxScore: 100,
+ weight: 1,
+ status: "good",
+ reason: "Baseline measurement",
+ },
+ ],
+ status: "good",
+ reason: "Baseline measurement",
+ },
+ {
+ name: "Bundle Strategy",
+ score: 100,
+ maxScore: 100,
+ weight: 1,
+ details: [
+ {
+ name: "Bundle Size",
+ score: 100,
+ maxScore: 100,
+ weight: 1,
+ status: "good",
+ reason: "Baseline measurement",
+ },
+ ],
+ status: "good",
+ reason: "Baseline measurement",
+ },
+ {
+ name: "Network Impact",
+ score: 100,
+ maxScore: 100,
+ weight: 1,
+ details: [
+ {
+ name: "Network Requests",
+ score: 100,
+ maxScore: 100,
+ weight: 1,
+ status: "good",
+ reason: "Baseline measurement",
+ },
+ ],
+ status: "good",
+ reason: "Baseline measurement",
+ },
+ {
+ name: "Transparency",
+ score: 100,
+ maxScore: 100,
+ weight: 1,
+ details: [
+ {
+ name: "Cookie Banner",
+ score: 100,
+ maxScore: 100,
+ weight: 1,
+ status: "good",
+ reason: "Baseline measurement",
+ },
+ ],
+ status: "good",
+ reason: "Baseline measurement",
+ },
+ {
+ name: "User Experience",
+ score: 100,
+ maxScore: 100,
+ weight: 1,
+ details: [
+ {
+ name: "User Experience",
+ score: 100,
+ maxScore: 100,
+ weight: 1,
+ status: "good",
+ reason: "Baseline measurement",
+ },
+ ],
+ status: "good",
+ reason: "Baseline measurement",
+ },
+ ],
+ insights: [],
+ recommendations: [],
+ };
+ }
+
+ // Create app data structure
+ const app: AppData = appData || {
+ name: "unknown",
+ baseline: false,
+ company: null,
+ techStack: "{}",
+ source: null,
+ tags: null,
+ };
+
+ // Create metrics data (scoring uses user-visible time only; cookieBannerVisibleTimeMs must be user-visible)
+ const metricsData: MetricsData = {
+ fcp: metrics.fcp,
+ lcp: metrics.lcp,
+ cls: metrics.cls,
+ tti: metrics.tti,
+ tbt: metrics.tbt,
+ totalSize: bundleMetrics.totalSize,
+ thirdPartySize: networkMetrics.thirdPartySize,
+ bannerVisibilityTime: transparencyMetrics.cookieBannerVisibleTimeMs || 0,
+ viewportCoverage: transparencyMetrics.cookieBannerCoverage * 100,
+ resourceCount: networkMetrics.totalRequests,
+ scriptLoadTime: 0, // TODO: Calculate from timing data
+ isBundled: networkMetrics.thirdPartyRequests === 0,
+ isIIFE: networkMetrics.thirdPartyRequests > 0,
+ // NEW: Add Perfume.js metrics
+ timeToFirstByte: metrics.timeToFirstByte || 0,
+ interactionToNextPaint: metrics.interactionToNextPaint,
+ networkInformation,
+ };
+
+ // Create mock resource data
+ const resourceData: ResourceData[] = [
+ { size: bundleMetrics.jsSize, isThirdParty: false },
+ { size: bundleMetrics.cssSize, isThirdParty: false },
+ ...Array.from({ length: networkMetrics.thirdPartyRequests }, () => ({
+ size:
+ networkMetrics.thirdPartySize /
+ Math.max(networkMetrics.thirdPartyRequests, 1),
+ isThirdParty: true,
+ })),
+ ];
+
+ // Create benchmark data (bannerRenderTime slot holds user-visible time for scoring; do not use DOM presence)
+ const benchmarkData: BenchmarkData = {
+ bannerRenderTime: transparencyMetrics.cookieBannerVisibleTimeMs || 0,
+ bannerInteractionTime: userExperienceMetrics.mainThreadBlocking,
+ layoutShift: userExperienceMetrics.layoutShifts,
+ };
+
+ // Parse app data
+ const techStack = parseTechStack(app.techStack);
+ const company = parseCompany(app.company);
+ const sourceInfo = parseSource(app.source);
+ const tags = app.tags || "";
+ const isOpenSource = isOpenSourceSolution(app, sourceInfo, tags);
+
+ // Calculate individual category scores
+ const performanceScore = calculatePerformanceScore(metricsData);
+ const bundleScore = calculateBundleScore(
+ metricsData,
+ techStack,
+ resourceData
+ );
+ const networkScore = calculateNetworkScore(metricsData, resourceData);
+ const transparencyScore = calculateTransparencyScore(
+ isOpenSource,
+ company,
+ techStack
+ );
+ const uxScore = calculateUXScore(metricsData, benchmarkData);
+
+ // Calculate weighted total score using more balanced weights
+ const weights = DEFAULT_SCORE_WEIGHTS;
+ const totalScore = Math.round(
+ (performanceScore.score / performanceScore.maxScore) *
+ 100 *
+ weights.performance +
+ (bundleScore.score / bundleScore.maxScore) *
+ 100 *
+ weights.bundleStrategy +
+ (networkScore.score / networkScore.maxScore) *
+ 100 *
+ weights.networkImpact +
+ (transparencyScore.score / transparencyScore.maxScore) *
+ 100 *
+ weights.transparency +
+ (uxScore.score / uxScore.maxScore) * 100 * weights.userExperience
+ );
+
+ // Create category scores
+ const categoryScores = {
+ performance: Math.round(
+ (performanceScore.score / performanceScore.maxScore) * 100
+ ),
+ bundleStrategy: Math.round(
+ (bundleScore.score / bundleScore.maxScore) * 100
+ ),
+ networkImpact: Math.round(
+ (networkScore.score / networkScore.maxScore) * 100
+ ),
+ transparency: Math.round(
+ (transparencyScore.score / transparencyScore.maxScore) * 100
+ ),
+ userExperience: Math.round((uxScore.score / uxScore.maxScore) * 100),
+ };
+
+ // Create score categories
+ const categories = [
+ {
+ name: "Performance",
+ score: performanceScore.score,
+ maxScore: performanceScore.maxScore,
+ weight: weights.performance,
+ details: performanceScore.details.map((d) => ({
+ name: d.metric,
+ score: d.score,
+ maxScore: d.maxScore,
+ weight: 1,
+ status: getCategoryStatus(d.score, d.maxScore),
+ reason: d.reason,
+ })),
+ status: getCategoryStatus(
+ performanceScore.score,
+ performanceScore.maxScore
+ ),
+ reason: `Performance score: ${performanceScore.score}/${performanceScore.maxScore}`,
+ },
+ {
+ name: "Bundle Strategy",
+ score: bundleScore.score,
+ maxScore: bundleScore.maxScore,
+ weight: weights.bundleStrategy,
+ details: bundleScore.details.map((d) => ({
+ name: d.metric,
+ score: d.score,
+ maxScore: d.maxScore,
+ weight: 1,
+ status: getCategoryStatus(d.score, d.maxScore),
+ reason: d.reason,
+ })),
+ status: getCategoryStatus(bundleScore.score, bundleScore.maxScore),
+ reason: `Bundle strategy score: ${bundleScore.score}/${bundleScore.maxScore}`,
+ },
+ {
+ name: "Network Impact",
+ score: networkScore.score,
+ maxScore: networkScore.maxScore,
+ weight: weights.networkImpact,
+ details: networkScore.details.map((d) => ({
+ name: d.metric,
+ score: d.score,
+ maxScore: d.maxScore,
+ weight: 1,
+ status: getCategoryStatus(d.score, d.maxScore),
+ reason: d.reason,
+ })),
+ status: getCategoryStatus(networkScore.score, networkScore.maxScore),
+ reason: `Network impact score: ${networkScore.score}/${networkScore.maxScore}`,
+ },
+ {
+ name: "Transparency",
+ score: transparencyScore.score,
+ maxScore: transparencyScore.maxScore,
+ weight: weights.transparency,
+ details: transparencyScore.details.map((d) => ({
+ name: d.metric,
+ score: d.score,
+ maxScore: d.maxScore,
+ weight: 1,
+ status: getCategoryStatus(d.score, d.maxScore),
+ reason: d.reason,
+ })),
+ status: getCategoryStatus(
+ transparencyScore.score,
+ transparencyScore.maxScore
+ ),
+ reason: `Transparency score: ${transparencyScore.score}/${transparencyScore.maxScore}`,
+ },
+ {
+ name: "User Experience",
+ score: uxScore.score,
+ maxScore: uxScore.maxScore,
+ weight: weights.userExperience,
+ details: uxScore.details.map((d) => ({
+ name: d.metric,
+ score: d.score,
+ maxScore: d.maxScore,
+ weight: 1,
+ status: getCategoryStatus(d.score, d.maxScore),
+ reason: d.reason,
+ })),
+ status: getCategoryStatus(uxScore.score, uxScore.maxScore),
+ reason: `User experience score: ${uxScore.score}/${uxScore.maxScore}`,
+ },
+ ];
+
+ // Generate insights and recommendations
+ const insights = generateInsights(
+ categoryScores,
+ metricsData,
+ resourceData,
+ isOpenSource
+ );
+ const recommendations = generateRecommendations(
+ categoryScores,
+ metricsData,
+ resourceData
+ );
+
+ return {
+ totalScore,
+ grade: getScoreGrade(totalScore),
+ categoryScores,
+ categories,
+ insights,
+ recommendations,
+ };
+}
+
+// Function to print scores in a table format
+// Note: This outputs directly to console as it's user-facing display output, not logging
+export function printScores(scores: BenchmarkScores): void {
+ // Create a table for overall scores
+ const overallTable = new Table({
+ head: ["Category", "Score", "Status"],
+ style: { head: ["cyan"] },
+ });
+
+ // Add overall score
+ overallTable.push(["Overall", `${scores.totalScore}/100`, scores.grade]);
+
+ // Add category scores
+ for (const category of scores.categories) {
+ overallTable.push([
+ category.name,
+ `${category.score}/100`,
+ category.status,
+ ]);
+ }
+
+ // Create a table for detailed scores
+ const detailsTable = new Table({
+ head: ["Category", "Metric", "Score", "Reason"],
+ style: { head: ["cyan"] },
+ });
+
+ // Add detailed scores
+ for (const category of scores.categories) {
+ for (const detail of category.details) {
+ detailsTable.push([
+ category.name,
+ detail.name,
+ `${detail.score}/100`,
+ detail.reason,
+ ]);
+ }
+ }
+
+ // biome-ignore lint/suspicious/noConsole: CLI output table
+ console.log(overallTable.toString());
+ // biome-ignore lint/suspicious/noConsole: CLI output table
+ console.log(detailsTable.toString());
+
+ if (scores.insights.length > 0) {
+ // biome-ignore lint/suspicious/noConsole: CLI output section
+ console.log("\nInsights:");
+ for (const insight of scores.insights) {
+ // biome-ignore lint/suspicious/noConsole: CLI output list
+ console.log(`- ${insight}`);
+ }
+ }
+
+ if (scores.recommendations.length > 0) {
+ // biome-ignore lint/suspicious/noConsole: CLI output section
+ console.log("\nRecommendations:");
+ for (const recommendation of scores.recommendations) {
+ // biome-ignore lint/suspicious/noConsole: CLI output list
+ console.log(`- ${recommendation}`);
+ }
+ }
+}
diff --git a/packages/cookiebench-cli/tsconfig.base.json b/packages/cookiebench-cli/tsconfig.base.json
new file mode 100644
index 0000000..5117f2a
--- /dev/null
+++ b/packages/cookiebench-cli/tsconfig.base.json
@@ -0,0 +1,19 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "compilerOptions": {
+ "declaration": true,
+ "declarationMap": true,
+ "esModuleInterop": true,
+ "incremental": false,
+ "isolatedModules": true,
+ "lib": ["es2022", "DOM", "DOM.Iterable"],
+ "module": "NodeNext",
+ "moduleDetection": "force",
+ "moduleResolution": "NodeNext",
+ "noUncheckedIndexedAccess": true,
+ "resolveJsonModule": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "target": "ES2022"
+ }
+}
diff --git a/packages/cookiebench-cli/tsconfig.json b/packages/cookiebench-cli/tsconfig.json
new file mode 100644
index 0000000..2a2c414
--- /dev/null
+++ b/packages/cookiebench-cli/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "esModuleInterop": true,
+ "strict": true,
+ "skipLibCheck": true,
+ "outDir": "dist",
+ "rootDir": "src"
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/packages/runner/README.md b/packages/runner/README.md
new file mode 100644
index 0000000..bfcd144
--- /dev/null
+++ b/packages/runner/README.md
@@ -0,0 +1,150 @@
+# @consentio/runner
+
+Benchmark orchestration for running cookie banner performance tests.
+
+## Overview
+
+This package orchestrates benchmark execution, managing browser instances, running iterations, aggregating results, and serving Next.js applications for testing.
+
+## Features
+
+- **Benchmark Orchestration**: Run multiple benchmark iterations with automated browser management
+- **Next.js Server Management**: Build and serve Next.js apps for local testing
+- **Performance Aggregation**: Calculate averages and aggregate metrics across iterations
+- **Config Loading**: Load and validate benchmark configurations
+- **Remote & Local Testing**: Support for both remote URLs and local development
+
+## Installation
+
+```bash
+pnpm add @consentio/runner @consentio/benchmark
+```
+
+## Usage
+
+### Basic Usage
+
+```typescript
+import { BenchmarkRunner, readConfig } from '@consentio/runner';
+
+// Load config
+const config = readConfig('./config.json');
+
+// Create runner
+const runner = new BenchmarkRunner(config);
+
+// Run benchmarks
+const results = await runner.runBenchmarks('http://localhost:3000');
+
+console.log('Benchmark complete:', results);
+```
+
+### With Server Management
+
+```typescript
+import {
+ BenchmarkRunner,
+ buildAndServeNextApp,
+ cleanupServer,
+ readConfig,
+} from '@consentio/runner';
+
+const config = readConfig();
+const serverInfo = await buildAndServeNextApp('./my-next-app');
+
+try {
+ const runner = new BenchmarkRunner(config);
+ const results = await runner.runBenchmarks(serverInfo.url);
+
+ console.log('Results:', results);
+} finally {
+ cleanupServer(serverInfo);
+}
+```
+
+### Remote Benchmarking
+
+```typescript
+import { BenchmarkRunner } from '@consentio/runner';
+
+const config = {
+ name: 'production-test',
+ iterations: 5,
+ remote: {
+ enabled: true,
+ url: 'https://production.example.com',
+ headers: {
+ 'Authorization': 'Bearer token',
+ },
+ },
+ // ... other config
+};
+
+const runner = new BenchmarkRunner(config);
+const results = await runner.runBenchmarks(config.remote.url);
+```
+
+## API
+
+### BenchmarkRunner
+
+- `constructor(config: Config)`: Create a new benchmark runner
+- `runBenchmarks(serverUrl: string)`: Run multiple benchmark iterations
+- `runSingleBenchmark(page: Page, url: string)`: Run a single benchmark iteration
+
+### Server Management
+
+- `buildAndServeNextApp(appPath?: string)`: Build and serve a Next.js app
+- `cleanupServer(serverInfo: ServerInfo)`: Stop the server process
+
+### Utilities
+
+- `readConfig(configPath?: string)`: Read and parse config.json
+- `formatTime(ms: number)`: Format milliseconds to human-readable string
+- `getPackageManager()`: Detect package manager (npm/yarn/pnpm)
+
+### PerformanceAggregator
+
+- `calculateTTI(coreWebVitals, cookieBannerData)`: Calculate Time to Interactive
+- `aggregateMetrics(...)`: Merge all collected metrics into final benchmark details
+- `calculateAverages(results)`: Calculate average metrics from multiple runs
+- `logResults(...)`: Log comprehensive benchmark results
+
+## Configuration
+
+```json
+{
+ "name": "my-app",
+ "iterations": 5,
+ "baseline": false,
+ "remote": {
+ "enabled": false,
+ "url": "https://example.com"
+ },
+ "cookieBanner": {
+ "selectors": [".cookie-banner"],
+ "serviceHosts": ["cookiecdn.com"],
+ "serviceName": "CookieService",
+ "waitForVisibility": true,
+ "measureViewportCoverage": true,
+ "expectedLayoutShift": true
+ },
+ "techStack": {
+ "bundler": "webpack",
+ "bundleType": "esm",
+ "frameworks": ["react", "nextjs"],
+ "languages": ["typescript"],
+ "packageManager": "pnpm",
+ "typescript": true
+ }
+}
+```
+
+## Types
+
+See the [types file](./src/types.ts) for complete type definitions.
+
+## License
+
+MIT
+
diff --git a/packages/runner/package.json b/packages/runner/package.json
new file mode 100644
index 0000000..70a112f
--- /dev/null
+++ b/packages/runner/package.json
@@ -0,0 +1,34 @@
+{
+ "name": "@consentio/runner",
+ "version": "0.0.1",
+ "type": "module",
+ "exports": {
+ ".": {
+ "import": "./dist/index.js",
+ "types": "./dist/index.d.ts"
+ }
+ },
+ "main": "./dist/index.js",
+ "module": "./dist/index.js",
+ "types": "./dist/index.d.ts",
+ "scripts": {
+ "build": "rslib build",
+ "check-types": "tsc --noEmit",
+ "dev": "rslib build --watch",
+ "fmt": "biome format . --write",
+ "lint": "biome lint ."
+ },
+ "dependencies": {
+ "@c15t/logger": "1.0.0",
+ "@consentio/benchmark": "workspace:*",
+ "@consentio/shared": "workspace:*",
+ "@playwright/test": "^1.58.2",
+ "playwright-performance-metrics": "^1.2.5"
+ },
+ "devDependencies": {
+ "@rsdoctor/rspack-plugin": "^1.5.2",
+ "@rslib/core": "^0.16.1",
+ "@types/node": "catalog:",
+ "typescript": "catalog:"
+ }
+}
diff --git a/packages/runner/rslib.config.ts b/packages/runner/rslib.config.ts
new file mode 100644
index 0000000..434432c
--- /dev/null
+++ b/packages/runner/rslib.config.ts
@@ -0,0 +1,14 @@
+import { defineConfig } from "@rslib/core";
+
+export default defineConfig({
+ lib: [
+ {
+ format: "esm",
+ syntax: "es2021",
+ dts: true,
+ },
+ ],
+ output: {
+ target: "node",
+ },
+});
diff --git a/packages/runner/src/benchmark-runner.ts b/packages/runner/src/benchmark-runner.ts
new file mode 100644
index 0000000..dedeb6d
--- /dev/null
+++ b/packages/runner/src/benchmark-runner.ts
@@ -0,0 +1,485 @@
+import { execFile } from "node:child_process";
+import { readFileSync, unlinkSync, writeFileSync } from "node:fs";
+import { join } from "node:path";
+import { promisify } from "node:util";
+import type { Logger } from "@c15t/logger";
+import type { Config } from "@consentio/benchmark";
+import {
+ BENCHMARK_CONSTANTS,
+ CookieBannerCollector,
+ NetworkMonitor,
+ PerfumeCollector,
+ ResourceTimingCollector,
+} from "@consentio/benchmark";
+import {
+ chromium,
+ type Browser,
+ type BrowserContext,
+ type Page,
+} from "@playwright/test";
+import { PerformanceMetricsCollector } from "playwright-performance-metrics";
+import { PerformanceAggregator } from "./performance-aggregator";
+import type { BenchmarkDetails, BenchmarkResult } from "./types";
+
+const execFileAsync = promisify(execFile);
+
+// Constants
+const WARMUP_ITERATIONS = 1; // Number of warmup runs before actual benchmarking
+const MAX_RETRIES = 2; // Maximum retries for failed iterations
+const ITERATION_TIMEOUT_MS = 120_000; // 2 minutes timeout per iteration
+const CLEANUP_DELAY_MS = 500; // Delay between iterations for cleanup
+const NAVIGATION_TIMEOUT_MS = 60_000; // 60 second timeout for navigation
+const RETRY_DELAY_MULTIPLIER = 2; // Multiplier for retry delay
+const MILLISECONDS_TO_SECONDS = 1000; // Conversion factor for time calculations
+
+export class BenchmarkRunner {
+ private readonly config: Config;
+ private readonly logger: Logger;
+ private readonly cookieBannerCollector: CookieBannerCollector;
+ private readonly networkMonitor: NetworkMonitor;
+ private readonly resourceTimingCollector: ResourceTimingCollector;
+ private readonly perfumeCollector: PerfumeCollector;
+ private readonly performanceAggregator: PerformanceAggregator;
+ private readonly saveTrace: boolean;
+ private readonly traceDir?: string;
+
+ constructor(
+ config: Config,
+ logger: Logger,
+ options?: { saveTrace?: boolean; traceDir?: string }
+ ) {
+ this.config = config;
+ this.logger = logger;
+ this.cookieBannerCollector = new CookieBannerCollector(config, logger);
+ this.networkMonitor = new NetworkMonitor(config, logger);
+ this.resourceTimingCollector = new ResourceTimingCollector(logger);
+ this.perfumeCollector = new PerfumeCollector(logger);
+ this.performanceAggregator = new PerformanceAggregator(logger);
+ this.saveTrace = options?.saveTrace ?? false;
+ this.traceDir = options?.traceDir;
+ this.validateConfig();
+ }
+
+ /**
+ * Validate configuration before running benchmarks
+ */
+ private validateConfig(): void {
+ if (!this.config.iterations || this.config.iterations < 1) {
+ throw new Error(
+ `Invalid iterations: ${this.config.iterations}. Must be at least 1.`
+ );
+ }
+
+ const hasSelectors =
+ this.config.cookieBanner?.selectors &&
+ this.config.cookieBanner.selectors.length > 0;
+ if (!hasSelectors) {
+ this.logger.warn(
+ "No cookie banner selectors configured. Banner detection may fail."
+ );
+ }
+
+ const hasWaitCondition =
+ this.config.testId || this.config.id || this.config.custom;
+
+ if (!hasWaitCondition) {
+ if (hasSelectors) {
+ this.logger.debug(
+ "No explicit wait condition, will use first cookie banner selector as fallback"
+ );
+ } else {
+ this.logger.warn(
+ "No wait condition configured (testId, id, or custom) and no cookie banner selectors found. Benchmarks may not wait for page readiness."
+ );
+ }
+ }
+ }
+
+ /**
+ * Run a single benchmark iteration with timeout and error handling
+ */
+ async runSingleBenchmark(
+ page: Page,
+ url: string,
+ isWarmup = false
+ ): Promise {
+ if (isWarmup) {
+ this.logger.debug(`Starting warmup benchmark for: ${url}`);
+ } else {
+ this.logger.debug(`Starting cookie banner benchmark for: ${url}`);
+ }
+ this.logger.debug(
+ "Cookie banner selectors:",
+ this.config.cookieBanner?.selectors || []
+ );
+ this.logger.debug(
+ "Bundle type from config:",
+ this.config.techStack?.bundleType
+ );
+
+ // Initialize collectors
+ const collector = new PerformanceMetricsCollector();
+ const cookieBannerMetrics = this.cookieBannerCollector.initializeMetrics();
+
+ // Setup monitoring and detection
+ await this.networkMonitor.setupMonitoring(page, url);
+ await this.cookieBannerCollector.setupDetection(page);
+ await this.perfumeCollector.setupPerfume(page);
+
+ // Navigate to the page with timeout
+ this.logger.debug(`Navigating to: ${url}`);
+ try {
+ await page.goto(url, {
+ waitUntil: "networkidle",
+ timeout: NAVIGATION_TIMEOUT_MS,
+ });
+ } catch (error) {
+ throw new Error(
+ `Navigation timeout or failed: ${error instanceof Error ? error.message : String(error)}`
+ );
+ }
+
+ // Wait for the specified element
+ await this.waitForElement(page);
+
+ // Wait for network to be idle
+ this.logger.debug("Waiting for network idle...");
+ await page.waitForLoadState("networkidle");
+
+ // Collect core web vitals from playwright-performance-metrics (primary source)
+ this.logger.debug("Collecting core web vitals...");
+ const coreWebVitals = await collector.collectMetrics(page, {
+ timeout: BENCHMARK_CONSTANTS.METRICS_TIMEOUT,
+ retryTimeout: BENCHMARK_CONSTANTS.METRICS_RETRY_TIMEOUT,
+ });
+
+ this.logger.debug("Core web vitals collected:", {
+ fcp: coreWebVitals.paint?.firstContentfulPaint,
+ lcp: coreWebVitals.largestContentfulPaint,
+ cls: coreWebVitals.cumulativeLayoutShift,
+ tbt: coreWebVitals.totalBlockingTime,
+ });
+
+ // Collect Perfume.js metrics (supplementary - TTFB, navigation timing, network info)
+ this.logger.debug("Collecting Perfume.js supplementary metrics...");
+ const perfumeMetrics = await this.perfumeCollector.collectMetrics(page);
+ this.logger.debug("Perfume.js metrics:", perfumeMetrics);
+
+ // Collect cookie banner specific metrics
+ const cookieBannerData =
+ await this.cookieBannerCollector.collectMetrics(page);
+ this.logger.debug("Cookie banner metrics:", cookieBannerData);
+
+ // Collect detailed resource timing data
+ const resourceMetrics = await this.resourceTimingCollector.collect(page);
+
+ // Get network metrics
+ const networkRequests = this.networkMonitor.getNetworkRequests();
+ const networkMetrics = this.networkMonitor.getMetrics();
+
+ // Aggregate all metrics
+ const finalMetrics = this.performanceAggregator.aggregateMetrics({
+ coreWebVitals,
+ cookieBannerData,
+ cookieBannerMetrics,
+ networkRequests,
+ networkMetrics,
+ resourceMetrics,
+ config: this.config,
+ perfumeMetrics,
+ });
+
+ // Log results
+ this.performanceAggregator.logResults(
+ finalMetrics,
+ cookieBannerMetrics,
+ this.config
+ );
+
+ // Cleanup
+ await collector.cleanup();
+ this.networkMonitor.reset();
+
+ return finalMetrics;
+ }
+
+ /**
+ * Run a single benchmark iteration with retry logic
+ */
+ private async runSingleBenchmarkWithRetry(
+ browser: Browser,
+ url: string,
+ isWarmup: boolean,
+ iterationNumber?: number
+ ): Promise {
+ let lastError: Error | null = null;
+
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt += 1) {
+ const context = await browser.newContext();
+ const page = await context.newPage();
+
+ try {
+ if (attempt > 0) {
+ this.logger.warn(
+ `Retrying iteration (attempt ${attempt + 1}/${MAX_RETRIES + 1})...`
+ );
+ }
+
+ if (this.saveTrace && !isWarmup) {
+ await context.tracing.start({
+ screenshots: true,
+ snapshots: true,
+ });
+ }
+
+ const result = await Promise.race([
+ this.runSingleBenchmark(page, url, isWarmup),
+ new Promise((_, reject) =>
+ setTimeout(
+ () => reject(new Error("Iteration timeout")),
+ ITERATION_TIMEOUT_MS
+ )
+ ),
+ ]);
+ if (this.saveTrace && !isWarmup && iterationNumber) {
+ await this.persistTrace(context, iterationNumber);
+ }
+ await context.close();
+ return result;
+ } catch (error) {
+ lastError = error instanceof Error ? error : new Error(String(error));
+ this.logger.debug(
+ `Iteration attempt ${attempt + 1} failed:`,
+ lastError.message
+ );
+ await context.close();
+
+ if (attempt < MAX_RETRIES) {
+ // Wait before retry
+ const retryDelay = CLEANUP_DELAY_MS * RETRY_DELAY_MULTIPLIER;
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
+ }
+ }
+ }
+
+ throw new Error(
+ `Failed to complete benchmark after ${MAX_RETRIES + 1} attempts: ${lastError?.message}`
+ );
+ }
+
+ private async persistTrace(
+ context: BrowserContext,
+ iterationNumber: number
+ ): Promise {
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
+ const traceZipPath = this.traceDir
+ ? join(this.traceDir, `Trace-${timestamp}.zip`)
+ : join(
+ process.cwd(),
+ `trace-${this.config.name}-iteration-${iterationNumber}.zip`
+ );
+ const traceJsonPath = this.traceDir
+ ? join(this.traceDir, `Trace-${timestamp}.json`)
+ : join(
+ process.cwd(),
+ `trace-${this.config.name}-iteration-${iterationNumber}.json`
+ );
+ await context.tracing.stop({ path: traceZipPath });
+
+ try {
+ const tempDir = this.traceDir || process.cwd();
+ await execFileAsync("unzip", [
+ "-o",
+ traceZipPath,
+ "-d",
+ tempDir,
+ "trace.trace",
+ ]);
+
+ const traceFilePath = join(tempDir, "trace.trace");
+ const traceContent = readFileSync(traceFilePath, "utf-8");
+ writeFileSync(traceJsonPath, traceContent, "utf-8");
+ try {
+ unlinkSync(traceFilePath);
+ } catch {
+ // Ignore cleanup failures
+ }
+ try {
+ unlinkSync(traceZipPath);
+ } catch {
+ // Ignore cleanup failures
+ }
+ this.logger.info(`📊 Trace saved to: ${traceJsonPath}`);
+ } catch {
+ this.logger.warn(
+ `Failed to extract trace JSON, keeping ZIP file: ${traceZipPath}`
+ );
+ this.logger.info(`📊 Trace saved to: ${traceZipPath}`);
+ }
+ }
+
+ /**
+ * Cleanup resources between iterations
+ */
+ private async cleanupBetweenIterations(): Promise {
+ // Small delay to allow cleanup
+ await new Promise((resolve) => setTimeout(resolve, CLEANUP_DELAY_MS));
+
+ // Force garbage collection if available (Node.js with --expose-gc)
+ if (global.gc) {
+ global.gc();
+ }
+ }
+
+ /**
+ * Run multiple benchmark iterations with warmup and error handling
+ */
+ async runBenchmarks(serverUrl: string): Promise {
+ const browser = await chromium.launch({
+ headless: true, // Keep headless mode for stability
+ args: ["--remote-debugging-port=9222"],
+ });
+ const results: BenchmarkDetails[] = [];
+ const startTime = Date.now();
+
+ try {
+ // Warmup runs (discarded, used to stabilize the environment)
+ if (WARMUP_ITERATIONS > 0) {
+ this.logger.info(
+ `Running ${WARMUP_ITERATIONS} warmup iteration(s) to stabilize environment...`
+ );
+ const warmupContext = await browser.newContext();
+ const warmupPage = await warmupContext.newPage();
+
+ for (let i = 0; i < WARMUP_ITERATIONS; i += 1) {
+ try {
+ await this.runSingleBenchmark(
+ warmupPage,
+ `${serverUrl}?t=${Date.now()}&warmup=true`,
+ true
+ );
+ this.logger.debug(`Warmup iteration ${i + 1} completed`);
+ } catch (error) {
+ this.logger.debug(
+ `Warmup iteration ${i + 1} failed (non-critical):`,
+ error instanceof Error ? error.message : String(error)
+ );
+ }
+ await this.cleanupBetweenIterations();
+ }
+
+ await warmupContext.close();
+ this.logger.info("Warmup complete. Starting actual benchmarks...");
+ }
+
+ // Actual benchmark iterations
+ for (let i = 0; i < this.config.iterations; i += 1) {
+ const iterationStartTime = Date.now();
+ const elapsedTimeSeconds = Math.round(
+ (Date.now() - startTime) / MILLISECONDS_TO_SECONDS
+ );
+ const avgTimePerIteration = i > 0 ? elapsedTimeSeconds / i : 0;
+ const remainingIterations = this.config.iterations - i - 1;
+ const estimatedRemaining = avgTimePerIteration * remainingIterations;
+
+ this.logger.info(
+ `Running iteration ${i + 1}/${this.config.iterations}${estimatedRemaining > 0 ? ` (est. ${Math.round(estimatedRemaining)}s remaining)` : ""}...`
+ );
+
+ try {
+ const result = await this.runSingleBenchmarkWithRetry(
+ browser,
+ // Add a timestamp to the URL to avoid caching
+ `${serverUrl}?t=${Date.now()}`,
+ false,
+ i + 1
+ );
+ results.push(result);
+
+ const iterationDurationSeconds = Math.round(
+ (Date.now() - iterationStartTime) / MILLISECONDS_TO_SECONDS
+ );
+ this.logger.debug(
+ `Iteration ${i + 1} completed in ${iterationDurationSeconds}s`
+ );
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error ? error.message : String(error);
+ this.logger.error(
+ `Failed to complete iteration ${i + 1}: ${errorMessage}`
+ );
+ throw error;
+ } finally {
+ await this.cleanupBetweenIterations();
+ }
+ }
+
+ if (results.length === 0) {
+ throw new Error(
+ "All benchmark iterations failed. Check logs for details."
+ );
+ }
+
+ if (results.length < this.config.iterations) {
+ this.logger.warn(
+ `Only ${results.length}/${this.config.iterations} iterations completed successfully. Results may be less reliable.`
+ );
+ }
+ } finally {
+ await browser.close();
+ }
+
+ const totalTimeSeconds = Math.round(
+ (Date.now() - startTime) / MILLISECONDS_TO_SECONDS
+ );
+ this.logger.info(
+ `Benchmark completed in ${totalTimeSeconds}s (${results.length} successful iterations)`
+ );
+
+ const averages = this.performanceAggregator.calculateAverages(results);
+
+ // Log statistical summary after all iterations
+ if (results.length > 1) {
+ this.performanceAggregator.logStatisticalSummary(results);
+ }
+
+ return {
+ name: this.config.name,
+ baseline: this.config.baseline ?? false,
+ techStack: this.config.techStack,
+ source: this.config.source,
+ includes: this.config.includes,
+ company: this.config.company,
+ tags: this.config.tags,
+ details: results,
+ average: averages,
+ };
+ }
+
+ /**
+ * Wait for the specified element based on config
+ * Falls back to first cookie banner selector if no explicit wait condition is set
+ */
+ private async waitForElement(page: Page): Promise {
+ if (this.config.testId) {
+ this.logger.debug(`Waiting for testId: ${this.config.testId}`);
+ await page.waitForSelector(`[data-testid="${this.config.testId}"]`);
+ } else if (this.config.id) {
+ this.logger.debug(`Waiting for id: ${this.config.id}`);
+ await page.waitForSelector(`#${this.config.id}`);
+ } else if (this.config.custom) {
+ this.logger.debug("Running custom wait function");
+ await this.config.custom(page);
+ } else {
+ // Fallback: use first cookie banner selector if available
+ const firstSelector = this.config.cookieBanner?.selectors?.[0];
+ if (firstSelector) {
+ this.logger.debug(
+ `No explicit wait condition found, using first cookie banner selector: ${firstSelector}`
+ );
+ await page.waitForSelector(firstSelector);
+ }
+ // If no selector found, continue without waiting (will rely on networkidle)
+ }
+ }
+}
diff --git a/packages/runner/src/index.ts b/packages/runner/src/index.ts
new file mode 100644
index 0000000..367833f
--- /dev/null
+++ b/packages/runner/src/index.ts
@@ -0,0 +1,31 @@
+// Main runner
+// biome-ignore lint/performance/noBarrelFile: this is a barrel file
+export { BenchmarkRunner } from "./benchmark-runner";
+// Performance aggregation
+export { PerformanceAggregator } from "./performance-aggregator";
+// Server management
+export { buildAndServeNextApp, cleanupServer } from "./server";
+// Types
+export type {
+ BenchmarkDetails,
+ BenchmarkResult,
+ BundleStrategy,
+ Config,
+ CookieBannerConfig,
+ CookieBannerData,
+ CookieBannerMetrics,
+ CoreWebVitals,
+ NetworkMetrics,
+ NetworkRequest,
+ ResourceTimingData,
+ ServerInfo,
+} from "./types";
+// Statistics utilities
+export {
+ calculateCoefficientOfVariation,
+ calculateStatistics,
+ calculateTrimmedMean,
+ isStable,
+} from "./statistics";
+// Utilities
+export { formatTime, getPackageManager, readConfig } from "./utils";
diff --git a/packages/runner/src/performance-aggregator.ts b/packages/runner/src/performance-aggregator.ts
new file mode 100644
index 0000000..c9d4cda
--- /dev/null
+++ b/packages/runner/src/performance-aggregator.ts
@@ -0,0 +1,473 @@
+import type { Logger } from "@c15t/logger";
+import type {
+ Config,
+ CookieBannerData,
+ CookieBannerMetrics,
+ CoreWebVitals,
+ NetworkMetrics,
+ NetworkRequest,
+ PerfumeMetrics,
+ ResourceTimingData,
+} from "@consentio/benchmark";
+import { PERCENTAGE_MULTIPLIER, TTI_BUFFER_MS } from "@consentio/shared";
+import {
+ calculateCoefficientOfVariation,
+ calculateStatistics,
+ calculateTrimmedMean,
+ isStable,
+} from "./statistics";
+import type { BenchmarkDetails, BenchmarkResult } from "./types";
+
+const VARIABILITY_WARNING_THRESHOLD = 20; // Coefficient of variation threshold for warnings
+const STABILITY_THRESHOLD = 15; // Coefficient of variation threshold for stability checks
+const TRIM_PERCENT = 10; // Percentage to trim from each end for trimmed mean
+
+type AggregateMetricsParams = {
+ coreWebVitals: CoreWebVitals;
+ cookieBannerData: CookieBannerData | null;
+ cookieBannerMetrics: CookieBannerMetrics;
+ networkRequests: NetworkRequest[];
+ networkMetrics: NetworkMetrics;
+ resourceMetrics: ResourceTimingData;
+ config: Config;
+ perfumeMetrics: PerfumeMetrics | null;
+};
+
+export class PerformanceAggregator {
+ private readonly logger: Logger;
+
+ constructor(logger: Logger) {
+ this.logger = logger;
+ }
+ /**
+ * Calculate Time to Interactive based on core web vitals and cookie banner interaction
+ */
+ calculateTTI(
+ coreWebVitals: CoreWebVitals,
+ cookieBannerData: CookieBannerData | null
+ ): number {
+ return (
+ Math.max(
+ coreWebVitals.paint?.firstContentfulPaint || 0,
+ coreWebVitals.domCompleteTiming || 0,
+ cookieBannerData?.bannerInteractiveTime || 0
+ ) + TTI_BUFFER_MS
+ ); // Add buffer for true interactivity
+ }
+
+ /**
+ * Build cookie banner timing metrics.
+ *
+ * Uses bannerVisibilityTime (opacity-based, user-perceived) as the primary
+ * visibility metric for scoring. This accounts for CSS animations and ensures
+ * scores reflect actual user experience. Falls back to interactiveTime if
+ * visibilityTime is not available.
+ *
+ * @param cookieBannerData Collected banner metrics from browser
+ * @param config Benchmark configuration
+ * @returns Cookie banner timing object with render, visibility, and interactive times
+ * @see METHODOLOGY.md for detailed explanation of visibility time vs render time
+ */
+ private buildCookieBannerTiming(
+ cookieBannerData: CookieBannerData | null,
+ config: Config
+ ) {
+ const domPresenceTime = cookieBannerData?.bannerRenderTime || 0;
+ const userVisibleTime =
+ cookieBannerData?.bannerVisibilityTime ||
+ cookieBannerData?.bannerInteractiveTime ||
+ 0;
+ return {
+ renderStart: domPresenceTime,
+ renderEnd: cookieBannerData?.bannerInteractiveTime || 0,
+ interactionStart: cookieBannerData?.bannerInteractiveTime || 0,
+ interactionEnd: cookieBannerData?.bannerInteractiveTime || 0,
+ layoutShift: cookieBannerData?.layoutShiftImpact || 0,
+ detected: cookieBannerData?.detected ?? false,
+ selector: cookieBannerData?.selector ?? null,
+ serviceName: config.cookieBanner?.serviceName ?? "unknown",
+ visibilityTime: userVisibleTime,
+ domPresenceTime,
+ userVisibleTime,
+ viewportCoverage: cookieBannerData?.viewportCoverage || 0,
+ };
+ }
+
+ /**
+ * Build third party metrics
+ */
+ private buildThirdPartyMetrics(
+ networkImpact: { totalImpact: number; totalDownloadTime: number },
+ networkMetrics: NetworkMetrics,
+ config: Config
+ ) {
+ return {
+ dnsLookupTime: 0,
+ connectionTime: 0,
+ downloadTime: networkImpact.totalDownloadTime,
+ totalImpact: networkImpact.totalImpact,
+ cookieServices: {
+ hosts: config.cookieBanner?.serviceHosts || [],
+ totalSize: networkMetrics.bannerBundleSize,
+ resourceCount: networkMetrics.bannerNetworkRequests,
+ dnsLookupTime: 0,
+ connectionTime: 0,
+ downloadTime: networkImpact.totalDownloadTime,
+ },
+ };
+ }
+
+ /**
+ * Build main thread blocking metrics
+ */
+ private buildMainThreadBlockingMetrics(
+ coreWebVitals: CoreWebVitals,
+ cookieBannerMetrics: CookieBannerMetrics
+ ) {
+ const totalBlockingTime = coreWebVitals.totalBlockingTime || 0;
+ const cookieBannerEstimate =
+ cookieBannerMetrics.bannerMainThreadBlockingTime;
+
+ const percentageFromCookies =
+ totalBlockingTime > 0
+ ? (cookieBannerEstimate / totalBlockingTime) * PERCENTAGE_MULTIPLIER
+ : 0;
+
+ return {
+ total: totalBlockingTime,
+ cookieBannerEstimate,
+ percentageFromCookies,
+ };
+ }
+
+ /**
+ * Merge all collected metrics into final benchmark details
+ */
+ aggregateMetrics(params: AggregateMetricsParams): BenchmarkDetails {
+ const {
+ coreWebVitals,
+ cookieBannerData,
+ cookieBannerMetrics,
+ networkRequests,
+ networkMetrics,
+ resourceMetrics,
+ config,
+ perfumeMetrics,
+ } = params;
+
+ const tti = this.calculateTTI(coreWebVitals, cookieBannerData);
+ const networkImpact = this.calculateNetworkImpact(networkRequests);
+
+ return {
+ duration: resourceMetrics.duration,
+ size: resourceMetrics.size,
+ timing: {
+ navigationStart: resourceMetrics.timing.navigationStart,
+ domContentLoaded: resourceMetrics.timing.domContentLoaded,
+ load: resourceMetrics.timing.load,
+ firstPaint: coreWebVitals.paint?.firstPaint || 0,
+ firstContentfulPaint: coreWebVitals.paint?.firstContentfulPaint || 0,
+ largestContentfulPaint: coreWebVitals.largestContentfulPaint || 0,
+ timeToInteractive: tti,
+ cumulativeLayoutShift: coreWebVitals.cumulativeLayoutShift || 0,
+ timeToFirstByte: perfumeMetrics?.timeToFirstByte ?? null,
+ firstInputDelay: perfumeMetrics?.firstInputDelay ?? null,
+ interactionToNextPaint: perfumeMetrics?.interactionToNextPaint ?? null,
+ navigationTiming: perfumeMetrics?.navigationTiming ?? {
+ timeToFirstByte: 0,
+ domInteractive: 0,
+ domContentLoadedEventStart: 0,
+ domContentLoadedEventEnd: 0,
+ domComplete: 0,
+ loadEventStart: 0,
+ loadEventEnd: 0,
+ },
+ networkInformation: perfumeMetrics?.networkInformation ?? undefined,
+ cookieBanner: this.buildCookieBannerTiming(cookieBannerData, config),
+ thirdParty: this.buildThirdPartyMetrics(
+ networkImpact,
+ networkMetrics,
+ config
+ ),
+ mainThreadBlocking: this.buildMainThreadBlockingMetrics(
+ coreWebVitals,
+ cookieBannerMetrics
+ ),
+ scripts: resourceMetrics.timing.scripts,
+ },
+ resources: resourceMetrics.resources,
+ language: resourceMetrics.language,
+ cookieBanner: {
+ detected: cookieBannerData?.detected ?? false,
+ selector: cookieBannerData?.selector ?? null,
+ serviceName: config.cookieBanner?.serviceName ?? "unknown",
+ visibilityTime:
+ cookieBannerData?.bannerVisibilityTime ||
+ cookieBannerData?.bannerInteractiveTime ||
+ 0,
+ domPresenceTime: cookieBannerData?.bannerRenderTime || 0,
+ userVisibleTime:
+ cookieBannerData?.bannerVisibilityTime ||
+ cookieBannerData?.bannerInteractiveTime ||
+ 0,
+ viewportCoverage: cookieBannerData?.viewportCoverage || 0,
+ },
+ thirdParty: {
+ cookieServices: {
+ hosts: config.cookieBanner?.serviceHosts || [],
+ totalSize: networkMetrics.bannerBundleSize,
+ resourceCount: networkMetrics.bannerNetworkRequests,
+ dnsLookupTime: 0,
+ connectionTime: 0,
+ downloadTime: networkImpact.totalDownloadTime,
+ },
+ totalImpact: networkImpact.totalImpact,
+ },
+ };
+ }
+
+ /**
+ * Calculate network impact metrics
+ */
+ private calculateNetworkImpact(networkRequests: NetworkRequest[]): {
+ totalImpact: number;
+ totalDownloadTime: number;
+ } {
+ const totalImpact = networkRequests.reduce((acc, req) => acc + req.size, 0);
+ const totalDownloadTime = networkRequests.reduce(
+ (acc, req) => acc + req.duration,
+ 0
+ );
+
+ return { totalImpact, totalDownloadTime };
+ }
+
+ /**
+ * Calculate average metrics from multiple benchmark results using Mitata statistics
+ * Uses trimmed mean (10% trim) for robustness against outliers
+ * Logs stability warnings for metrics with high variability
+ */
+ calculateAverages(results: BenchmarkDetails[]): BenchmarkResult["average"] {
+ if (results.length === 0) {
+ throw new Error("Cannot calculate averages from empty results array");
+ }
+
+ // Extract metric arrays for statistical analysis
+ const fcpValues = results.map((r) => r.timing.firstContentfulPaint);
+ const lcpValues = results.map((r) => r.timing.largestContentfulPaint);
+ const ttiValues = results.map((r) => r.timing.timeToInteractive);
+ const tbtValues = results.map((r) => r.timing.mainThreadBlocking.total);
+ const ttfbValues = results
+ .map((r) => r.timing.timeToFirstByte)
+ .filter((value): value is number => value !== null && value > 0);
+ const fidValues = results
+ .map((r) => r.timing.firstInputDelay || 0)
+ .filter((v) => v > 0);
+ const inpValues = results
+ .map((r) => r.timing.interactionToNextPaint || 0)
+ .filter((v) => v > 0);
+ const clsValues = results.map((r) => r.timing.cumulativeLayoutShift);
+ const totalSizeValues = results.map((r) => r.size.total);
+ const jsSizeValues = results.map((r) => r.size.scripts.total);
+ const cssSizeValues = results.map((r) => r.size.styles);
+ const imageSizeValues = results.map((r) => r.size.images);
+ const fontSizeValues = results.map((r) => r.size.fonts);
+ const otherSizeValues = results.map((r) => r.size.other);
+ const totalRequestsValues = results.map(
+ (r) =>
+ r.resources.scripts.length +
+ r.resources.styles.length +
+ r.resources.images.length +
+ r.resources.fonts.length +
+ r.resources.other.length
+ );
+ const domContentLoadedValues = results.map(
+ (r) => r.timing.domContentLoaded
+ );
+ const loadValues = results.map((r) => r.timing.load);
+
+ // Use trimmed mean for better robustness against outliers (10% trim)
+ // Log stability warnings for critical metrics
+ if (!isStable(fcpValues, VARIABILITY_WARNING_THRESHOLD)) {
+ this.logger.warn(
+ `First Contentful Paint shows high variability (CV: ${calculateCoefficientOfVariation(fcpValues).toFixed(1)}%)`
+ );
+ }
+ if (!isStable(lcpValues, VARIABILITY_WARNING_THRESHOLD)) {
+ this.logger.warn(
+ `Largest Contentful Paint shows high variability (CV: ${calculateCoefficientOfVariation(lcpValues).toFixed(1)}%)`
+ );
+ }
+ if (!isStable(ttiValues, VARIABILITY_WARNING_THRESHOLD)) {
+ this.logger.warn(
+ `Time to Interactive shows high variability (CV: ${calculateCoefficientOfVariation(ttiValues).toFixed(1)}%)`
+ );
+ }
+
+ return {
+ firstContentfulPaint: calculateTrimmedMean(fcpValues, TRIM_PERCENT),
+ largestContentfulPaint: calculateTrimmedMean(lcpValues, TRIM_PERCENT),
+ timeToInteractive: calculateTrimmedMean(ttiValues, TRIM_PERCENT),
+ totalBlockingTime: calculateTrimmedMean(tbtValues, TRIM_PERCENT),
+ speedIndex: 0, // Default value
+ timeToFirstByte:
+ ttfbValues.length > 0
+ ? calculateTrimmedMean(ttfbValues, TRIM_PERCENT)
+ : 0,
+ firstInputDelay:
+ fidValues.length > 0
+ ? calculateTrimmedMean(fidValues, TRIM_PERCENT)
+ : 0,
+ interactionToNextPaint:
+ inpValues.length > 0
+ ? calculateTrimmedMean(inpValues, TRIM_PERCENT)
+ : 0,
+ cumulativeLayoutShift: calculateTrimmedMean(clsValues, TRIM_PERCENT),
+ domSize: 0, // Default value
+ totalRequests: calculateTrimmedMean(totalRequestsValues, TRIM_PERCENT),
+ totalSize: calculateTrimmedMean(totalSizeValues, TRIM_PERCENT),
+ jsSize: calculateTrimmedMean(jsSizeValues, TRIM_PERCENT),
+ cssSize: calculateTrimmedMean(cssSizeValues, TRIM_PERCENT),
+ imageSize: calculateTrimmedMean(imageSizeValues, TRIM_PERCENT),
+ fontSize: calculateTrimmedMean(fontSizeValues, TRIM_PERCENT),
+ otherSize: calculateTrimmedMean(otherSizeValues, TRIM_PERCENT),
+ thirdPartyRequests: 0, // Default value
+ thirdPartySize: 0, // Default value
+ thirdPartyDomains: 0, // Default value
+ thirdPartyCookies: 0, // Default value
+ thirdPartyLocalStorage: 0, // Default value
+ thirdPartySessionStorage: 0, // Default value
+ thirdPartyIndexedDB: 0, // Default value
+ thirdPartyCache: 0, // Default value
+ thirdPartyServiceWorkers: 0, // Default value
+ thirdPartyWebWorkers: 0, // Default value
+ thirdPartyWebSockets: 0, // Default value
+ thirdPartyBeacons: 0, // Default value
+ thirdPartyFetch: 0, // Default value
+ thirdPartyXHR: 0, // Default value
+ thirdPartyScripts: 0, // Default value
+ thirdPartyStyles: 0, // Default value
+ thirdPartyImages: 0, // Default value
+ thirdPartyFonts: 0, // Default value
+ thirdPartyMedia: 0, // Default value
+ thirdPartyOther: 0, // Default value
+ thirdPartyTiming: {
+ total: 0,
+ blocking: 0,
+ dns: 0,
+ connect: 0,
+ ssl: 0,
+ send: 0,
+ wait: 0,
+ receive: 0,
+ },
+ cookieBannerTiming: {
+ firstPaint: 0,
+ firstContentfulPaint: calculateTrimmedMean(fcpValues, TRIM_PERCENT),
+ domContentLoaded: calculateTrimmedMean(
+ domContentLoadedValues,
+ TRIM_PERCENT
+ ),
+ load: calculateTrimmedMean(loadValues, TRIM_PERCENT),
+ },
+ };
+ }
+
+ /**
+ * Get statistical summary for a set of benchmark results
+ */
+ getStatisticalSummary(results: BenchmarkDetails[]): {
+ fcp: ReturnType;
+ lcp: ReturnType;
+ tti: ReturnType;
+ tbt: ReturnType;
+ } {
+ const fcpValues = results.map((r) => r.timing.firstContentfulPaint);
+ const lcpValues = results.map((r) => r.timing.largestContentfulPaint);
+ const ttiValues = results.map((r) => r.timing.timeToInteractive);
+ const tbtValues = results.map((r) => r.timing.mainThreadBlocking.total);
+
+ return {
+ fcp: calculateStatistics(fcpValues),
+ lcp: calculateStatistics(lcpValues),
+ tti: calculateStatistics(ttiValues),
+ tbt: calculateStatistics(tbtValues),
+ };
+ }
+
+ /**
+ * Log comprehensive benchmark results with statistical information
+ */
+ logResults(
+ finalMetrics: BenchmarkDetails,
+ cookieBannerMetrics: CookieBannerMetrics,
+ config: Config
+ ): void {
+ let bundleStrategy = "Unknown";
+ if (cookieBannerMetrics.isBundled) {
+ bundleStrategy = "Bundled";
+ } else if (cookieBannerMetrics.isIIFE) {
+ bundleStrategy = "IIFE";
+ }
+
+ this.logger.debug("Final cookie banner benchmark results:", {
+ fcp: finalMetrics.timing.firstContentfulPaint,
+ lcp: finalMetrics.timing.largestContentfulPaint,
+ cls: finalMetrics.timing.cumulativeLayoutShift,
+ tti: finalMetrics.timing.timeToInteractive,
+ tbt: finalMetrics.timing.mainThreadBlocking.total,
+ bannerDetected: finalMetrics.cookieBanner.detected,
+ bannerRenderTime: Math.max(
+ 0,
+ finalMetrics.timing.cookieBanner.renderEnd -
+ finalMetrics.timing.cookieBanner.renderStart
+ ),
+ bannerLayoutShift: finalMetrics.timing.cookieBanner.layoutShift,
+ bannerNetworkImpact: finalMetrics.thirdParty.totalImpact,
+ bundleStrategy,
+ isBundled: cookieBannerMetrics.isBundled,
+ isIIFE: cookieBannerMetrics.isIIFE,
+ configBundleType: config.techStack?.bundleType,
+ });
+ }
+
+ /**
+ * Log statistical summary for multiple benchmark runs
+ */
+ logStatisticalSummary(results: BenchmarkDetails[]): void {
+ if (results.length === 0) {
+ return;
+ }
+
+ const summary = this.getStatisticalSummary(results);
+
+ this.logger.info("📊 Statistical Summary:");
+ this.logger.info(
+ ` FCP: ${summary.fcp.mean.toFixed(0)}ms (median: ${summary.fcp.median.toFixed(0)}ms, stddev: ${summary.fcp.stddev.toFixed(0)}ms)`
+ );
+ this.logger.info(
+ ` LCP: ${summary.lcp.mean.toFixed(0)}ms (median: ${summary.lcp.median.toFixed(0)}ms, stddev: ${summary.lcp.stddev.toFixed(0)}ms)`
+ );
+ this.logger.info(
+ ` TTI: ${summary.tti.mean.toFixed(0)}ms (median: ${summary.tti.median.toFixed(0)}ms, stddev: ${summary.tti.stddev.toFixed(0)}ms)`
+ );
+ this.logger.info(
+ ` TBT: ${summary.tbt.mean.toFixed(0)}ms (median: ${summary.tbt.median.toFixed(0)}ms, stddev: ${summary.tbt.stddev.toFixed(0)}ms)`
+ );
+
+ // Log stability indicators
+ const fcpValues = results.map((r) => r.timing.firstContentfulPaint);
+ const lcpValues = results.map((r) => r.timing.largestContentfulPaint);
+ const ttiValues = results.map((r) => r.timing.timeToInteractive);
+
+ if (isStable(fcpValues, STABILITY_THRESHOLD)) {
+ this.logger.info(" ✓ FCP is stable");
+ }
+ if (isStable(lcpValues, STABILITY_THRESHOLD)) {
+ this.logger.info(" ✓ LCP is stable");
+ }
+ if (isStable(ttiValues, STABILITY_THRESHOLD)) {
+ this.logger.info(" ✓ TTI is stable");
+ }
+ }
+}
diff --git a/packages/runner/src/server.ts b/packages/runner/src/server.ts
new file mode 100644
index 0000000..b071c13
--- /dev/null
+++ b/packages/runner/src/server.ts
@@ -0,0 +1,125 @@
+import { spawn } from "node:child_process";
+import type { Logger } from "@c15t/logger";
+import type { ServerInfo } from "./types";
+import { getPackageManager, ONE_SECOND } from "./utils";
+
+export async function buildAndServeNextApp(
+ logger: Logger,
+ appPath?: string
+): Promise {
+ const pm = await getPackageManager();
+ const cwd = appPath || process.cwd();
+
+ // Build the app
+ logger.info("Building Next.js app...");
+ const buildProcess = spawn(pm.command, [...pm.args, "build"], {
+ cwd,
+ stdio: "inherit",
+ });
+
+ await new Promise((resolve, reject) => {
+ let settled = false;
+ const onError = (error: Error) => {
+ if (settled) {
+ return;
+ }
+ settled = true;
+ reject(
+ new Error(
+ `Build process failed to start or crashed early: ${error.message}`
+ )
+ );
+ };
+ const onClose = (code: number | null) => {
+ if (settled) {
+ return;
+ }
+ settled = true;
+ if (code === 0) {
+ resolve();
+ } else {
+ reject(new Error(`Build failed with code ${code}`));
+ }
+ };
+ buildProcess.once("error", onError);
+ buildProcess.once("close", onClose);
+ });
+
+ // Start the server
+ logger.info("Starting Next.js server...");
+ // biome-ignore lint/style/noMagicNumbers: working with random port
+ const port = Math.floor(Math.random() * (9000 - 3000 + 1)) + 3000;
+ logger.debug("Server command:", [
+ ...pm.args,
+ "start",
+ "--",
+ "--port",
+ port.toString(),
+ ]);
+ const serverProcess = spawn(
+ pm.command,
+ [...pm.args, "start", "--", "--port", port.toString()],
+ {
+ cwd,
+ stdio: ["inherit", "pipe", "inherit"],
+ }
+ );
+
+ // Wait for server to be ready
+ const url = `http://localhost:${port}`;
+ let retries = 0;
+ const maxRetries = 30;
+ const requestTimeoutMs = 5000;
+ let crashErrorMessage: string | null = null;
+ const onServerExit = (code: number | null, signal: NodeJS.Signals | null) => {
+ crashErrorMessage = `Server process exited before ready (code: ${code}, signal: ${signal})`;
+ };
+ const onServerError = (error: Error) => {
+ crashErrorMessage = `Server process failed before ready: ${error.message}`;
+ };
+ serverProcess.once("exit", onServerExit);
+ serverProcess.once("error", onServerError);
+
+ while (retries < maxRetries) {
+ if (crashErrorMessage !== null) {
+ logger.error(crashErrorMessage);
+ serverProcess.kill();
+ serverProcess.removeListener("exit", onServerExit);
+ serverProcess.removeListener("error", onServerError);
+ throw new Error(crashErrorMessage);
+ }
+
+ const controller = new AbortController();
+ const timeoutHandle = setTimeout(() => {
+ controller.abort();
+ }, requestTimeoutMs);
+ try {
+ const response = await fetch(url, { signal: controller.signal });
+ if (response.ok) {
+ clearTimeout(timeoutHandle);
+ serverProcess.removeListener("exit", onServerExit);
+ serverProcess.removeListener("error", onServerError);
+ logger.success("Server is ready!");
+ return { serverProcess, url };
+ }
+ } catch {
+ // Ignore error and retry
+ } finally {
+ clearTimeout(timeoutHandle);
+ }
+
+ await new Promise((resolve) => setTimeout(resolve, ONE_SECOND));
+ retries += 1;
+ }
+
+ serverProcess.removeListener("exit", onServerExit);
+ serverProcess.removeListener("error", onServerError);
+ serverProcess.kill();
+ throw new Error("Server failed to start");
+}
+
+export function cleanupServer(serverInfo: ServerInfo): void {
+ if (serverInfo.serverProcess) {
+ serverInfo.serverProcess.kill();
+ }
+}
diff --git a/packages/runner/src/statistics.ts b/packages/runner/src/statistics.ts
new file mode 100644
index 0000000..fd2f488
--- /dev/null
+++ b/packages/runner/src/statistics.ts
@@ -0,0 +1,146 @@
+// Constants for statistics calculations
+const PERCENTILE_95 = 95;
+const PERCENTILE_99 = 99;
+const DEFAULT_TRIM_PERCENT = 10;
+const PERCENTAGE_CONVERSION = 100;
+const STABILITY_THRESHOLD = 15;
+
+/**
+ * Calculate statistical metrics for an array of numbers (inspired by Mitata's approach)
+ */
+export function calculateStatistics(values: number[]): {
+ mean: number;
+ median: number;
+ stddev: number;
+ min: number;
+ max: number;
+ p95: number;
+ p99: number;
+} {
+ if (values.length === 0) {
+ return {
+ mean: 0,
+ median: 0,
+ stddev: 0,
+ min: 0,
+ max: 0,
+ p95: 0,
+ p99: 0,
+ };
+ }
+
+ const sorted = [...values].sort((a, b) => a - b);
+ const mean = values.reduce((a, b) => a + b, 0) / values.length;
+ const variance =
+ values.reduce((acc, val) => acc + (val - mean) ** 2, 0) / values.length;
+ const stddev = Math.sqrt(variance);
+ const median = getMedian(sorted);
+ const p95 = getPercentile(sorted, PERCENTILE_95);
+ const p99 = getPercentile(sorted, PERCENTILE_99);
+
+ const lastIndex = sorted.length - 1;
+ const maxValue = lastIndex >= 0 ? sorted[lastIndex] : 0;
+ return {
+ mean,
+ median,
+ stddev,
+ min: sorted[0],
+ max: maxValue,
+ p95,
+ p99,
+ };
+}
+
+/**
+ * Calculate median from sorted array
+ */
+function getMedian(sorted: number[]): number {
+ const mid = Math.floor(sorted.length / 2);
+ if (sorted.length % 2 === 0) {
+ return (sorted[mid - 1] + sorted[mid]) / 2;
+ }
+ return sorted[mid];
+}
+
+/**
+ * Calculate percentile from sorted array
+ */
+function getPercentile(sorted: number[], percentile: number): number {
+ const index = (percentile / PERCENTAGE_CONVERSION) * (sorted.length - 1);
+ const lower = Math.floor(index);
+ const upper = Math.ceil(index);
+ const weight = index - lower;
+
+ if (lower === upper) {
+ return sorted[lower];
+ }
+
+ return sorted[lower] * (1 - weight) + sorted[upper] * weight;
+}
+
+/**
+ * Calculate robust average using trimmed mean (removes outliers)
+ */
+export function calculateTrimmedMean(
+ values: number[],
+ trimPercent = DEFAULT_TRIM_PERCENT
+): number {
+ if (values.length === 0) {
+ return 0;
+ }
+
+ if (values.length <= 2) {
+ return values.reduce((a, b) => a + b, 0) / values.length;
+ }
+
+ const sorted = [...values].sort((a, b) => a - b);
+ const normalizedTrimPercent = Math.max(
+ 0,
+ Math.min(trimPercent, PERCENTAGE_CONVERSION / 2)
+ );
+ const requestedTrimCount = Math.floor(
+ (values.length * normalizedTrimPercent) / PERCENTAGE_CONVERSION
+ );
+ const trimCount = Math.max(
+ 0,
+ Math.min(requestedTrimCount, Math.floor(values.length / 2))
+ );
+ const trimmed = sorted.slice(trimCount, values.length - trimCount);
+
+ if (trimmed.length === 0) {
+ return sorted[Math.floor(sorted.length / 2)];
+ }
+
+ return trimmed.reduce((a, b) => a + b, 0) / trimmed.length;
+}
+
+/**
+ * Calculate coefficient of variation (CV) as a measure of stability
+ */
+export function calculateCoefficientOfVariation(values: number[]): number {
+ if (values.length === 0) {
+ return 0;
+ }
+
+ const mean = values.reduce((a, b) => a + b, 0) / values.length;
+ const variance =
+ values.reduce((acc, val) => acc + (val - mean) ** 2, 0) / values.length;
+ const stddev = Math.sqrt(variance);
+
+ if (mean === 0) {
+ return 0;
+ }
+
+ return (stddev / mean) * PERCENTAGE_CONVERSION;
+}
+
+/**
+ * Check if values are statistically stable (low coefficient of variation)
+ */
+export function isStable(
+ values: number[],
+ threshold = STABILITY_THRESHOLD
+): boolean {
+ const cv = calculateCoefficientOfVariation(values);
+ return cv < threshold;
+}
diff --git a/packages/runner/src/types.ts b/packages/runner/src/types.ts
new file mode 100644
index 0000000..9b458ed
--- /dev/null
+++ b/packages/runner/src/types.ts
@@ -0,0 +1,302 @@
+import type { ChildProcess } from "node:child_process";
+import type { BundleType } from "@consentio/benchmark";
+
+// Re-export common types from benchmark package
+export type {
+ BundleType,
+ BundleStrategy,
+ Config,
+ CookieBannerConfig,
+ CookieBannerData,
+ CookieBannerMetrics,
+ CoreWebVitals,
+ NetworkMetrics,
+ NetworkRequest,
+ PerfumeMetrics,
+ ResourceTimingData,
+} from "@consentio/benchmark";
+
+// Server types
+export type ServerInfo = {
+ serverProcess: ChildProcess;
+ url: string;
+};
+
+// Benchmark result types
+export type BenchmarkDetails = {
+ duration: number;
+ size: {
+ total: number;
+ bundled: number;
+ thirdParty: number;
+ scripts: {
+ total: number;
+ initial: number;
+ dynamic: number;
+ };
+ styles: number;
+ images: number;
+ fonts: number;
+ other: number;
+ };
+ timing: {
+ navigationStart: number;
+ domContentLoaded: number;
+ load: number;
+ firstPaint: number;
+ firstContentfulPaint: number;
+ largestContentfulPaint: number;
+ timeToInteractive: number;
+ cumulativeLayoutShift: number;
+ // Enhanced metrics from Perfume.js
+ timeToFirstByte: number | null;
+ firstInputDelay: number | null;
+ interactionToNextPaint: number | null;
+ navigationTiming: {
+ timeToFirstByte: number;
+ domInteractive: number;
+ domContentLoadedEventStart: number;
+ domContentLoadedEventEnd: number;
+ domComplete: number;
+ loadEventStart: number;
+ loadEventEnd: number;
+ };
+ networkInformation?: {
+ effectiveType: string;
+ downlink: number;
+ rtt: number;
+ saveData: boolean;
+ };
+ cookieBanner: {
+ renderStart: number;
+ renderEnd: number;
+ interactionStart: number;
+ interactionEnd: number;
+ layoutShift: number;
+ detected: boolean;
+ selector: string | null;
+ serviceName: string;
+ visibilityTime: number | null;
+ /** DOM presence time (ms). Alias for renderStart; explicit for downstream consumers. */
+ domPresenceTime: number;
+ /** User-visible time (ms). Alias for visibilityTime; used for scoring. */
+ userVisibleTime: number;
+ viewportCoverage: number;
+ };
+ thirdParty: {
+ dnsLookupTime: number;
+ connectionTime: number;
+ downloadTime: number;
+ totalImpact: number;
+ cookieServices: {
+ hosts: string[];
+ totalSize: number;
+ resourceCount: number;
+ dnsLookupTime: number;
+ connectionTime: number;
+ downloadTime: number;
+ };
+ };
+ mainThreadBlocking: {
+ total: number;
+ cookieBannerEstimate: number;
+ percentageFromCookies: number;
+ };
+ scripts: {
+ bundled: {
+ loadStart: number;
+ loadEnd: number;
+ executeStart: number;
+ executeEnd: number;
+ };
+ thirdParty: {
+ loadStart: number;
+ loadEnd: number;
+ executeStart: number;
+ executeEnd: number;
+ };
+ };
+ };
+ language: string;
+ resources: {
+ scripts: Array<{
+ name: string;
+ size: number;
+ duration: number;
+ startTime: number;
+ isThirdParty: boolean;
+ isDynamic: boolean;
+ }>;
+ styles: Array<{
+ name: string;
+ size: number;
+ duration: number;
+ startTime: number;
+ isThirdParty: boolean;
+ }>;
+ images: Array<{
+ name: string;
+ size: number;
+ duration: number;
+ startTime: number;
+ isThirdParty: boolean;
+ }>;
+ fonts: Array<{
+ name: string;
+ size: number;
+ duration: number;
+ startTime: number;
+ isThirdParty: boolean;
+ }>;
+ other: Array<{
+ name: string;
+ size: number;
+ duration: number;
+ startTime: number;
+ isThirdParty: boolean;
+ type: string;
+ }>;
+ };
+ dom?: {
+ size?: number;
+ };
+ cookieBanner: EnhancedCookieBannerTiming;
+ thirdParty: ThirdPartyMetrics;
+};
+
+export type BenchmarkResult = {
+ name: string;
+ baseline: boolean;
+ techStack: {
+ bundler: string;
+ bundleType: BundleType | BundleType[];
+ frameworks: string[];
+ languages: string[];
+ packageManager: string;
+ typescript: boolean;
+ };
+ source: {
+ github: string | false;
+ isOpenSource: boolean | string;
+ license: string;
+ npm: string | false;
+ website?: string;
+ };
+ includes: {
+ backend: string | string[] | false;
+ components: string[];
+ };
+ company?: {
+ name: string;
+ website: string;
+ avatar: string;
+ };
+ tags?: string[];
+ details: BenchmarkDetails[];
+ average: {
+ firstContentfulPaint: number;
+ largestContentfulPaint: number;
+ timeToInteractive: number;
+ totalBlockingTime: number;
+ speedIndex: number;
+ timeToFirstByte: number;
+ firstInputDelay: number;
+ interactionToNextPaint: number;
+ cumulativeLayoutShift: number;
+ domSize: number;
+ totalRequests: number;
+ totalSize: number;
+ jsSize: number;
+ cssSize: number;
+ imageSize: number;
+ fontSize: number;
+ otherSize: number;
+ thirdPartyRequests: number;
+ thirdPartySize: number;
+ thirdPartyDomains: number;
+ thirdPartyCookies: number;
+ thirdPartyLocalStorage: number;
+ thirdPartySessionStorage: number;
+ thirdPartyIndexedDB: number;
+ thirdPartyCache: number;
+ thirdPartyServiceWorkers: number;
+ thirdPartyWebWorkers: number;
+ thirdPartyWebSockets: number;
+ thirdPartyBeacons: number;
+ thirdPartyFetch: number;
+ thirdPartyXHR: number;
+ thirdPartyScripts: number;
+ thirdPartyStyles: number;
+ thirdPartyImages: number;
+ thirdPartyFonts: number;
+ thirdPartyMedia: number;
+ thirdPartyOther: number;
+ thirdPartyTiming: {
+ total: number;
+ blocking: number;
+ dns: number;
+ connect: number;
+ ssl: number;
+ send: number;
+ wait: number;
+ receive: number;
+ };
+ cookieBannerTiming: {
+ firstPaint: number;
+ firstContentfulPaint: number;
+ domContentLoaded: number;
+ load: number;
+ };
+ };
+ scores?: {
+ totalScore: number;
+ grade: "Excellent" | "Good" | "Fair" | "Poor" | "Critical";
+ categoryScores: {
+ performance: number;
+ bundleStrategy: number;
+ networkImpact: number;
+ transparency: number;
+ userExperience: number;
+ };
+ categories: Array<{
+ name: string;
+ score: number;
+ maxScore: number;
+ weight: number;
+ details: Array<{
+ name: string;
+ score: number;
+ maxScore: number;
+ weight: number;
+ status: "excellent" | "good" | "fair" | "poor";
+ reason: string;
+ }>;
+ status: "excellent" | "good" | "fair" | "poor";
+ reason: string;
+ }>;
+ insights: string[];
+ recommendations: string[];
+ };
+};
+
+type EnhancedCookieBannerTiming = {
+ detected: boolean;
+ selector: string | null;
+ serviceName: string;
+ visibilityTime: number | null;
+ domPresenceTime: number;
+ userVisibleTime: number;
+ viewportCoverage: number;
+};
+
+type ThirdPartyMetrics = {
+ cookieServices: {
+ hosts: string[];
+ totalSize: number;
+ resourceCount: number;
+ dnsLookupTime: number;
+ connectionTime: number;
+ downloadTime: number;
+ };
+ totalImpact: number;
+};
diff --git a/packages/runner/src/utils.ts b/packages/runner/src/utils.ts
new file mode 100644
index 0000000..6b0f42c
--- /dev/null
+++ b/packages/runner/src/utils.ts
@@ -0,0 +1,57 @@
+import { readFileSync } from "node:fs";
+import { join } from "node:path";
+import type { Config } from "@consentio/benchmark";
+
+export const ONE_SECOND = 1000;
+export function readConfig(configPath?: string): Config | null {
+ try {
+ const path = configPath || join(process.cwd(), "config.json");
+ const configContent = readFileSync(path, "utf-8");
+ return JSON.parse(configContent) as Config;
+ } catch (error) {
+ // biome-ignore lint/suspicious/noConsole: console error is needed for debugging
+ console.error("Failed to read config.json:", error);
+ return null;
+ }
+}
+
+export function formatTime(ms: number): string {
+ if (ms < ONE_SECOND) {
+ return `${ms.toFixed(0)}ms`;
+ }
+ return `${(ms / ONE_SECOND).toFixed(2)}s`;
+}
+
+export async function getPackageManager(): Promise<{
+ command: string;
+ args: string[];
+}> {
+ try {
+ const { execSync } = await import("node:child_process");
+ const output = execSync("npm -v", { encoding: "utf-8" });
+ if (output) {
+ return { command: "npm", args: ["run"] };
+ }
+ } catch {
+ try {
+ const { execSync } = await import("node:child_process");
+ const output = execSync("yarn -v", { encoding: "utf-8" });
+ if (output) {
+ return { command: "yarn", args: [] };
+ }
+ } catch {
+ try {
+ const { execSync } = await import("node:child_process");
+ const output = execSync("pnpm -v", { encoding: "utf-8" });
+ if (output) {
+ return { command: "pnpm", args: [] };
+ }
+ } catch {
+ // Default to npm if no package manager is found
+ return { command: "npm", args: ["run"] };
+ }
+ }
+ }
+ // Default to npm if no package manager is found
+ return { command: "npm", args: ["run"] };
+}
diff --git a/packages/runner/tsconfig.json b/packages/runner/tsconfig.json
new file mode 100644
index 0000000..fde9b9d
--- /dev/null
+++ b/packages/runner/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "esModuleInterop": true,
+ "strict": true,
+ "skipLibCheck": true,
+ "declaration": true,
+ "outDir": "dist",
+ "rootDir": "src"
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/packages/shared/README.md b/packages/shared/README.md
new file mode 100644
index 0000000..a72627b
--- /dev/null
+++ b/packages/shared/README.md
@@ -0,0 +1,99 @@
+# @consentio/shared
+
+Shared utilities, constants, and helper functions used across all Consentio benchmark packages.
+
+## Purpose
+
+This package contains common functionality that is used by multiple packages in the monorepo:
+- `@consentio/benchmark` - Core benchmarking logic
+- `@consentio/runner` - Benchmark orchestration
+- `cookiebench` - CLI tool
+
+## Contents
+
+### Constants
+
+- **Time constants**: `ONE_SECOND` (1000ms), `HALF_SECOND` (500ms), `TTI_BUFFER_MS` (1000ms)
+- **Size constants**: `BYTES_TO_KB` (1024), `KILOBYTE` (1024)
+- **Percentage constants**: `PERCENTAGE_MULTIPLIER` (100), `PERCENTAGE_DIVISOR` (100)
+
+### Utilities
+
+#### Time Formatting
+- **`formatTime(ms: number): string`** - Convert milliseconds to human-readable format
+ - Returns `"150ms"` for values < 1 second
+ - Returns `"1.50s"` for values ≥ 1 second
+
+#### Byte Formatting
+- **`formatBytes(bytes: number): string`** - Format bytes to human-readable string with appropriate units
+ - Returns `"0 bytes"`, `"1.50 KB"`, `"2.00 MB"`, etc.
+
+#### Config Management
+- **`readConfig(path?: string): T | null`** - Read and parse JSON config files
+ - Defaults to `./config.json` if no path provided
+ - Returns `null` if file cannot be read
+ - Generic type parameter allows for type-safe config objects
+
+#### Package Manager Detection
+- **`getPackageManager(): Promise<{ command: string; args: string[] }>`** - Detect and return available package manager (npm/yarn/pnpm)
+
+#### Conversion Helpers
+- **`bytesToKB(bytes: number): number`** - Convert bytes to kilobytes
+- **`decimalToPercentage(decimal: number): number`** - Convert 0.75 → 75
+- **`percentageToDecimal(percentage: number): number`** - Convert 75 → 0.75
+
+## Usage
+
+```typescript
+import {
+ ONE_SECOND,
+ formatTime,
+ formatBytes,
+ readConfig,
+ bytesToKB
+} from '@consentio/shared';
+
+// Format time
+console.log(formatTime(1500)); // "1.50s"
+console.log(formatTime(150)); // "150ms"
+
+// Format bytes
+console.log(formatBytes(1536)); // "1.50 KB"
+console.log(formatBytes(2097152)); // "2.00 MB"
+
+// Read config
+const config = readConfig('./my-config.json');
+
+// Use constants
+import { setTimeout } from "node:timers/promises"; // Promise-based setTimeout (Node.js 16+)
+await setTimeout(ONE_SECOND);
+
+// Convert sizes
+const sizeInKB = bytesToKB(2048); // 2
+```
+
+## Integration Status
+
+✅ **@consentio/benchmark** - Fully integrated
+ - Uses shared constants for time and size conversions
+ - Imports directly via `@consentio/shared`
+
+✅ **@consentio/runner** - Fully integrated
+ - Re-exports shared utilities with proper Config typing
+ - Provides typed wrappers: `formatTime`, `getPackageManager`, `readConfig`
+
+✅ **cookiebench CLI** - Fully integrated
+ - Re-exports shared utilities and constants via `utils/index.ts`
+ - Provides typed `readConfig` wrapper for CLI Config type
+ - All duplicate implementations removed
+
+## Benefits
+
+- ✅ Single source of truth for shared functionality
+- ✅ No code duplication across packages
+- ✅ Easier maintenance and testing
+- ✅ Smaller bundle sizes
+- ✅ Type-safe utilities with proper TypeScript support
+- ✅ Consistent behavior across all packages
+
+
diff --git a/packages/shared/package.json b/packages/shared/package.json
new file mode 100644
index 0000000..976b9ff
--- /dev/null
+++ b/packages/shared/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "@consentio/shared",
+ "version": "0.0.1",
+ "type": "module",
+ "exports": {
+ ".": {
+ "import": "./dist/index.js",
+ "types": "./dist/index.d.ts"
+ }
+ },
+ "main": "./dist/index.js",
+ "module": "./dist/index.js",
+ "types": "./dist/index.d.ts",
+ "scripts": {
+ "build": "rslib build",
+ "check-types": "tsc --noEmit",
+ "dev": "rslib build --watch",
+ "fmt": "biome format . --write",
+ "lint": "biome lint ."
+ },
+ "devDependencies": {
+ "@cookiebench/ts-config": "workspace:*",
+ "@rsdoctor/rspack-plugin": "^1.5.2",
+ "@rslib/core": "^0.16.1",
+ "@types/node": "^24.0.9",
+ "typescript": "catalog:"
+ }
+}
diff --git a/packages/shared/rslib.config.ts b/packages/shared/rslib.config.ts
new file mode 100644
index 0000000..a782543
--- /dev/null
+++ b/packages/shared/rslib.config.ts
@@ -0,0 +1,19 @@
+import { defineConfig } from "@rslib/core";
+
+export default defineConfig({
+ lib: [
+ {
+ format: "esm",
+ syntax: "es2021",
+ dts: true,
+ },
+ ],
+ source: {
+ entry: {
+ index: "./src/index.ts",
+ },
+ },
+ output: {
+ target: "node",
+ },
+});
diff --git a/packages/shared/src/constants.ts b/packages/shared/src/constants.ts
new file mode 100644
index 0000000..fd73f95
--- /dev/null
+++ b/packages/shared/src/constants.ts
@@ -0,0 +1,14 @@
+// Time constants
+export const ONE_SECOND = 1000;
+export const HALF_SECOND = 500;
+
+// Size constants (bytes to kilobytes)
+export const BYTES_TO_KB = 1024;
+export const KILOBYTE = 1024;
+
+// Percentage constants
+export const PERCENTAGE_MULTIPLIER = 100;
+export const PERCENTAGE_DIVISOR = 100;
+
+// Common thresholds
+export const TTI_BUFFER_MS = 1000; // Buffer for true interactivity
diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts
new file mode 100644
index 0000000..9765f12
--- /dev/null
+++ b/packages/shared/src/index.ts
@@ -0,0 +1,22 @@
+/** biome-ignore-all lint/performance/noBarrelFile: this is a barrel file */
+
+// Constants
+export {
+ BYTES_TO_KB,
+ HALF_SECOND,
+ KILOBYTE,
+ ONE_SECOND,
+ PERCENTAGE_DIVISOR,
+ PERCENTAGE_MULTIPLIER,
+ TTI_BUFFER_MS,
+} from "./constants";
+export { type BaseConfig, readConfig } from "./utils/config";
+// Utilities
+export {
+ bytesToKB,
+ decimalToPercentage,
+ formatBytes,
+ percentageToDecimal,
+} from "./utils/conversion";
+export { getPackageManager } from "./utils/package-manager";
+export { formatTime } from "./utils/time";
diff --git a/packages/shared/src/utils/config.ts b/packages/shared/src/utils/config.ts
new file mode 100644
index 0000000..11fe1a2
--- /dev/null
+++ b/packages/shared/src/utils/config.ts
@@ -0,0 +1,35 @@
+import { readFileSync } from "node:fs";
+import { join } from "node:path";
+
+/**
+ * Generic config type - packages should define their own specific config types
+ */
+export type BaseConfig = Record;
+
+/**
+ * Read and parse a JSON config file
+ *
+ * Note: `readConfig` only applies the generic `T` type at compile time.
+ * It does not validate JSON structure at runtime. If strict runtime safety is
+ * required, validate the parsed value for the provided `configPath` before
+ * using the returned value.
+ *
+ * @param configPath - Optional path to config file, defaults to ./config.json
+ * @returns Parsed config object or null if file cannot be read
+ */
+export function readConfig(
+ configPath?: string
+): T | null {
+ try {
+ const resolvedPath = configPath || join(process.cwd(), "config.json");
+ const configContent = readFileSync(resolvedPath, "utf-8");
+ return JSON.parse(configContent) as T;
+ } catch (error) {
+ // biome-ignore lint/suspicious/noConsole: console error is needed for debugging
+ console.error(
+ `Failed to read config at ${configPath || join(process.cwd(), "config.json")}:`,
+ error
+ );
+ return null;
+ }
+}
diff --git a/packages/shared/src/utils/conversion.ts b/packages/shared/src/utils/conversion.ts
new file mode 100644
index 0000000..c6c6f35
--- /dev/null
+++ b/packages/shared/src/utils/conversion.ts
@@ -0,0 +1,46 @@
+import { BYTES_TO_KB, KILOBYTE, PERCENTAGE_MULTIPLIER } from "../constants";
+
+/**
+ * Convert bytes to kilobytes
+ * @param bytes - Size in bytes
+ * @returns Size in kilobytes
+ */
+export function bytesToKB(bytes: number): number {
+ return bytes / BYTES_TO_KB;
+}
+
+/**
+ * Format bytes to human-readable string with appropriate units
+ * @param bytes - Size in bytes
+ * @returns Formatted string (e.g., "1.50 KB", "2.00 MB")
+ */
+export function formatBytes(bytes: number): string {
+ if (!Number.isFinite(bytes) || bytes <= 0) {
+ return "0 bytes";
+ }
+ const safeBytes = Math.max(0, bytes);
+ const sizes = ["bytes", "KB", "MB", "GB", "TB", "PB"];
+ const rawIndex = Math.floor(Math.log(safeBytes) / Math.log(KILOBYTE));
+ const unitIndex = Math.max(0, Math.min(rawIndex, sizes.length - 1));
+ const normalizedValue = safeBytes / KILOBYTE ** unitIndex;
+ const safeValue = Number.isFinite(normalizedValue) ? normalizedValue : 0;
+ return `${Number.parseFloat(safeValue.toFixed(2))} ${sizes[unitIndex]}`;
+}
+
+/**
+ * Convert decimal to percentage
+ * @param decimal - Decimal value (e.g., 0.75)
+ * @returns Percentage value (e.g., 75)
+ */
+export function decimalToPercentage(decimal: number): number {
+ return decimal * PERCENTAGE_MULTIPLIER;
+}
+
+/**
+ * Convert percentage to decimal
+ * @param percentage - Percentage value (e.g., 75)
+ * @returns Decimal value (e.g., 0.75)
+ */
+export function percentageToDecimal(percentage: number): number {
+ return percentage / PERCENTAGE_MULTIPLIER;
+}
diff --git a/packages/shared/src/utils/package-manager.ts b/packages/shared/src/utils/package-manager.ts
new file mode 100644
index 0000000..d3dffab
--- /dev/null
+++ b/packages/shared/src/utils/package-manager.ts
@@ -0,0 +1,85 @@
+import { execSync } from "node:child_process";
+import { existsSync, readFileSync } from "node:fs";
+import { join } from "node:path";
+
+type PackageManagerInfo = {
+ command: string;
+ args: string[];
+};
+
+function fromName(name: string): PackageManagerInfo | null {
+ if (name.startsWith("pnpm")) {
+ return { command: "pnpm", args: [] };
+ }
+ if (name.startsWith("yarn")) {
+ return { command: "yarn", args: [] };
+ }
+ if (name.startsWith("npm")) {
+ return { command: "npm", args: ["run"] };
+ }
+ return null;
+}
+
+/**
+ * Detect and return the available package manager
+ * @returns Package manager command and args
+ */
+export function getPackageManager(): PackageManagerInfo {
+ const userAgent = process.env.npm_config_user_agent || "";
+ const userAgentResult = fromName(userAgent);
+ if (userAgentResult) {
+ return userAgentResult;
+ }
+
+ try {
+ const rootPackageJsonPath = join(process.cwd(), "package.json");
+ if (existsSync(rootPackageJsonPath)) {
+ const packageJson = JSON.parse(
+ readFileSync(rootPackageJsonPath, "utf-8")
+ ) as {
+ packageManager?: string;
+ };
+ if (packageJson.packageManager) {
+ const packageManagerResult = fromName(packageJson.packageManager);
+ if (packageManagerResult) {
+ return packageManagerResult;
+ }
+ }
+ }
+ } catch {
+ // Ignore parse/read errors and continue fallback detection
+ }
+
+ if (existsSync(join(process.cwd(), "pnpm-lock.yaml"))) {
+ return { command: "pnpm", args: [] };
+ }
+ if (existsSync(join(process.cwd(), "yarn.lock"))) {
+ return { command: "yarn", args: [] };
+ }
+
+ try {
+ const output = execSync("npm -v", { encoding: "utf-8" });
+ if (output) {
+ return { command: "npm", args: ["run"] };
+ }
+ } catch {
+ try {
+ const output = execSync("yarn -v", { encoding: "utf-8" });
+ if (output) {
+ return { command: "yarn", args: [] };
+ }
+ } catch {
+ try {
+ const output = execSync("pnpm -v", { encoding: "utf-8" });
+ if (output) {
+ return { command: "pnpm", args: [] };
+ }
+ } catch {
+ // Default to npm if no package manager is found
+ return { command: "npm", args: ["run"] };
+ }
+ }
+ }
+ // Fallback if all checks succeed but output is falsy (shouldn't happen in practice)
+ return { command: "npm", args: ["run"] };
+}
diff --git a/packages/shared/src/utils/time.ts b/packages/shared/src/utils/time.ts
new file mode 100644
index 0000000..b2c4064
--- /dev/null
+++ b/packages/shared/src/utils/time.ts
@@ -0,0 +1,20 @@
+import { ONE_SECOND } from "../constants";
+
+/**
+ * Format milliseconds to human-readable time string
+ * @param ms - Time in milliseconds
+ * @returns Formatted time string (e.g., "150ms" or "1.50s")
+ */
+export function formatTime(ms: number): string {
+ if (!Number.isFinite(ms)) {
+ return "N/A";
+ }
+ if (ms < 0) {
+ return "0ms";
+ }
+
+ if (ms < ONE_SECOND) {
+ return `${ms.toFixed(0)}ms`;
+ }
+ return `${(ms / ONE_SECOND).toFixed(2)}s`;
+}
diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json
new file mode 100644
index 0000000..2a2c414
--- /dev/null
+++ b/packages/shared/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "esModuleInterop": true,
+ "strict": true,
+ "skipLibCheck": true,
+ "outDir": "dist",
+ "rootDir": "src"
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 1a07d0f..20df7d6 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -4,92 +4,463 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
+catalogs:
+ default:
+ '@types/node':
+ specifier: 25.2.3
+ version: 25.2.3
+ '@types/react':
+ specifier: 19.2.14
+ version: 19.2.14
+ '@types/react-dom':
+ specifier: 19.2.3
+ version: 19.2.3
+ next:
+ specifier: 16.1.6
+ version: 16.1.6
+ react:
+ specifier: 19.2.4
+ version: 19.2.4
+ react-dom:
+ specifier: 19.2.4
+ version: 19.2.4
+ typescript:
+ specifier: 5.9.3
+ version: 5.9.3
+
importers:
.:
devDependencies:
'@biomejs/biome':
- specifier: 2.3.8
- version: 2.3.8
- '@c15t/translations':
- specifier: ^1.8.0
- version: 1.8.0
- '@cookiebench/cli':
+ specifier: 2.3.2
+ version: 2.3.2
+ '@consentio/benchmark':
specifier: workspace:*
- version: link:packages/cli
+ version: link:packages/benchmark
+ '@consentio/runner':
+ specifier: workspace:*
+ version: link:packages/runner
'@playwright/test':
- specifier: ^1.57.0
- version: 1.57.0
+ specifier: ^1.58.2
+ version: 1.58.2
cli-table3:
specifier: ^0.6.5
version: 0.6.5
+ cookiebench:
+ specifier: workspace:*
+ version: link:packages/cookiebench-cli
drizzle-kit:
- specifier: ^0.31.8
- version: 0.31.8
- p-limit:
- specifier: ^7.2.0
- version: 7.2.0
+ specifier: ^0.31.9
+ version: 0.31.9
pretty-ms:
specifier: ^9.3.0
version: 9.3.0
turbo:
- specifier: ^2.6.3
- version: 2.6.3
+ specifier: ^2.8.9
+ version: 2.8.9
typescript:
specifier: 5.9.3
version: 5.9.3
ultracite:
- specifier: ^6.3.9
- version: 6.3.9(@orpc/server@1.12.2(@opentelemetry/api@1.9.0)(ws@8.18.3))(typescript@5.9.3)
+ specifier: 6.0.5
+ version: 6.0.5(@orpc/server@1.8.1(@opentelemetry/api@1.9.0)(ws@8.18.3))(typescript@5.9.3)
benchmarks/baseline:
dependencies:
next:
- specifier: 16.0.7
- version: 16.0.7(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+ specifier: 'catalog:'
+ version: 16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react:
- specifier: ^19.2.1
- version: 19.2.1
+ specifier: 'catalog:'
+ version: 19.2.4
react-dom:
- specifier: ^19.2.1
- version: 19.2.1(react@19.2.1)
+ specifier: 'catalog:'
+ version: 19.2.4(react@19.2.4)
devDependencies:
'@cookiebench/benchmark-schema':
specifier: workspace:*
version: link:../../packages/benchmark-schema
- '@cookiebench/cli':
+ '@cookiebench/ts-config':
specifier: workspace:*
- version: link:../../packages/cli
+ version: link:../../packages/typescript-config
+ '@types/node':
+ specifier: 'catalog:'
+ version: 25.2.3
+ '@types/react':
+ specifier: 'catalog:'
+ version: 19.2.14
+ '@types/react-dom':
+ specifier: 'catalog:'
+ version: 19.2.3(@types/react@19.2.14)
+ typescript:
+ specifier: 'catalog:'
+ version: 5.9.3
+
+ benchmarks/c15t-nextjs:
+ dependencies:
+ '@c15t/nextjs':
+ specifier: 1.7.1
+ version: 1.7.1(@opentelemetry/api@1.9.0)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(next@16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(ws@8.18.3)
+ next:
+ specifier: 'catalog:'
+ version: 16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ react:
+ specifier: 'catalog:'
+ version: 19.2.4
+ react-dom:
+ specifier: 'catalog:'
+ version: 19.2.4(react@19.2.4)
+ devDependencies:
+ '@cookiebench/benchmark-schema':
+ specifier: workspace:*
+ version: link:../../packages/benchmark-schema
+ '@cookiebench/ts-config':
+ specifier: workspace:*
+ version: link:../../packages/typescript-config
+ '@types/node':
+ specifier: 'catalog:'
+ version: 25.2.3
+ '@types/react':
+ specifier: 'catalog:'
+ version: 19.2.14
+ '@types/react-dom':
+ specifier: 'catalog:'
+ version: 19.2.3(@types/react@19.2.14)
+ typescript:
+ specifier: 'catalog:'
+ version: 5.9.3
+
+ benchmarks/c15t-react:
+ dependencies:
+ '@c15t/react':
+ specifier: 1.7.1
+ version: 1.7.1(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(@opentelemetry/instrumentation@0.207.0(@opentelemetry/api@1.9.0))(@types/better-sqlite3@7.6.13)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(better-sqlite3@12.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typeorm@0.3.27(better-sqlite3@12.4.1)(reflect-metadata@0.2.2))(use-sync-external-store@1.6.0(react@19.2.4))(ws@8.18.3)
+ next:
+ specifier: 'catalog:'
+ version: 16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ react:
+ specifier: 'catalog:'
+ version: 19.2.4
+ react-dom:
+ specifier: 'catalog:'
+ version: 19.2.4(react@19.2.4)
+ devDependencies:
+ '@cookiebench/benchmark-schema':
+ specifier: workspace:*
+ version: link:../../packages/benchmark-schema
+ '@cookiebench/ts-config':
+ specifier: workspace:*
+ version: link:../../packages/typescript-config
+ '@types/node':
+ specifier: 'catalog:'
+ version: 25.2.3
+ '@types/react':
+ specifier: 'catalog:'
+ version: 19.2.14
+ '@types/react-dom':
+ specifier: 'catalog:'
+ version: 19.2.3(@types/react@19.2.14)
+ typescript:
+ specifier: 'catalog:'
+ version: 5.9.3
+
+ benchmarks/cookie-control:
+ dependencies:
+ next:
+ specifier: 'catalog:'
+ version: 16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ react:
+ specifier: 'catalog:'
+ version: 19.2.4
+ react-dom:
+ specifier: 'catalog:'
+ version: 19.2.4(react@19.2.4)
+ devDependencies:
+ '@cookiebench/benchmark-schema':
+ specifier: workspace:*
+ version: link:../../packages/benchmark-schema
+ '@cookiebench/ts-config':
+ specifier: workspace:*
+ version: link:../../packages/typescript-config
+ '@types/node':
+ specifier: 'catalog:'
+ version: 25.2.3
+ '@types/react':
+ specifier: 'catalog:'
+ version: 19.2.14
+ '@types/react-dom':
+ specifier: 'catalog:'
+ version: 19.2.3(@types/react@19.2.14)
+ typescript:
+ specifier: 'catalog:'
+ version: 5.9.3
+
+ benchmarks/cookie-yes:
+ dependencies:
+ next:
+ specifier: 'catalog:'
+ version: 16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ react:
+ specifier: 'catalog:'
+ version: 19.2.4
+ react-dom:
+ specifier: 'catalog:'
+ version: 19.2.4(react@19.2.4)
+ devDependencies:
+ '@cookiebench/benchmark-schema':
+ specifier: workspace:*
+ version: link:../../packages/benchmark-schema
+ '@cookiebench/ts-config':
+ specifier: workspace:*
+ version: link:../../packages/typescript-config
+ '@types/node':
+ specifier: 'catalog:'
+ version: 25.2.3
+ '@types/react':
+ specifier: 'catalog:'
+ version: 19.2.14
+ '@types/react-dom':
+ specifier: 'catalog:'
+ version: 19.2.3(@types/react@19.2.14)
+ typescript:
+ specifier: 'catalog:'
+ version: 5.9.3
+
+ benchmarks/didomi:
+ dependencies:
+ '@didomi/react':
+ specifier: ^1.8.8
+ version: 1.8.8(react@19.2.4)
+ next:
+ specifier: 'catalog:'
+ version: 16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ react:
+ specifier: 'catalog:'
+ version: 19.2.4
+ react-dom:
+ specifier: 'catalog:'
+ version: 19.2.4(react@19.2.4)
+ devDependencies:
+ '@cookiebench/benchmark-schema':
+ specifier: workspace:*
+ version: link:../../packages/benchmark-schema
+ '@cookiebench/ts-config':
+ specifier: workspace:*
+ version: link:../../packages/typescript-config
+ '@types/node':
+ specifier: 'catalog:'
+ version: 25.2.3
+ '@types/react':
+ specifier: 'catalog:'
+ version: 19.2.14
+ '@types/react-dom':
+ specifier: 'catalog:'
+ version: 19.2.3(@types/react@19.2.14)
+ typescript:
+ specifier: 'catalog:'
+ version: 5.9.3
+
+ benchmarks/enzuzo:
+ dependencies:
+ next:
+ specifier: 'catalog:'
+ version: 16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ react:
+ specifier: 'catalog:'
+ version: 19.2.4
+ react-dom:
+ specifier: 'catalog:'
+ version: 19.2.4(react@19.2.4)
+ devDependencies:
+ '@cookiebench/benchmark-schema':
+ specifier: workspace:*
+ version: link:../../packages/benchmark-schema
+ '@cookiebench/ts-config':
+ specifier: workspace:*
+ version: link:../../packages/typescript-config
+ '@types/node':
+ specifier: 'catalog:'
+ version: 25.2.3
+ '@types/react':
+ specifier: 'catalog:'
+ version: 19.2.14
+ '@types/react-dom':
+ specifier: 'catalog:'
+ version: 19.2.3(@types/react@19.2.14)
+ typescript:
+ specifier: 'catalog:'
+ version: 5.9.3
+
+ benchmarks/iubenda:
+ dependencies:
+ next:
+ specifier: 'catalog:'
+ version: 16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ react:
+ specifier: 'catalog:'
+ version: 19.2.4
+ react-dom:
+ specifier: 'catalog:'
+ version: 19.2.4(react@19.2.4)
+ devDependencies:
+ '@cookiebench/benchmark-schema':
+ specifier: workspace:*
+ version: link:../../packages/benchmark-schema
+ '@cookiebench/ts-config':
+ specifier: workspace:*
+ version: link:../../packages/typescript-config
+ '@types/node':
+ specifier: 'catalog:'
+ version: 25.2.3
+ '@types/react':
+ specifier: 'catalog:'
+ version: 19.2.14
+ '@types/react-dom':
+ specifier: 'catalog:'
+ version: 19.2.3(@types/react@19.2.14)
+ typescript:
+ specifier: 'catalog:'
+ version: 5.9.3
+
+ benchmarks/ketch:
+ dependencies:
+ next:
+ specifier: 'catalog:'
+ version: 16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ react:
+ specifier: 'catalog:'
+ version: 19.2.4
+ react-dom:
+ specifier: 'catalog:'
+ version: 19.2.4(react@19.2.4)
+ devDependencies:
+ '@cookiebench/benchmark-schema':
+ specifier: workspace:*
+ version: link:../../packages/benchmark-schema
+ '@cookiebench/ts-config':
+ specifier: workspace:*
+ version: link:../../packages/typescript-config
+ '@types/node':
+ specifier: 'catalog:'
+ version: 25.2.3
+ '@types/react':
+ specifier: 'catalog:'
+ version: 19.2.14
+ '@types/react-dom':
+ specifier: 'catalog:'
+ version: 19.2.3(@types/react@19.2.14)
+ typescript:
+ specifier: 'catalog:'
+ version: 5.9.3
+
+ benchmarks/onetrust:
+ dependencies:
+ next:
+ specifier: 'catalog:'
+ version: 16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ react:
+ specifier: 'catalog:'
+ version: 19.2.4
+ react-dom:
+ specifier: 'catalog:'
+ version: 19.2.4(react@19.2.4)
+ devDependencies:
+ '@cookiebench/benchmark-schema':
+ specifier: workspace:*
+ version: link:../../packages/benchmark-schema
+ '@cookiebench/ts-config':
+ specifier: workspace:*
+ version: link:../../packages/typescript-config
+ '@types/node':
+ specifier: 'catalog:'
+ version: 25.2.3
+ '@types/react':
+ specifier: 'catalog:'
+ version: 19.2.14
+ '@types/react-dom':
+ specifier: 'catalog:'
+ version: 19.2.3(@types/react@19.2.14)
+ typescript:
+ specifier: 'catalog:'
+ version: 5.9.3
+
+ benchmarks/osano:
+ dependencies:
+ next:
+ specifier: 'catalog:'
+ version: 16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ react:
+ specifier: 'catalog:'
+ version: 19.2.4
+ react-dom:
+ specifier: 'catalog:'
+ version: 19.2.4(react@19.2.4)
+ devDependencies:
+ '@cookiebench/benchmark-schema':
+ specifier: workspace:*
+ version: link:../../packages/benchmark-schema
+ '@cookiebench/ts-config':
+ specifier: workspace:*
+ version: link:../../packages/typescript-config
+ '@types/node':
+ specifier: 'catalog:'
+ version: 25.2.3
+ '@types/react':
+ specifier: 'catalog:'
+ version: 19.2.14
+ '@types/react-dom':
+ specifier: 'catalog:'
+ version: 19.2.3(@types/react@19.2.14)
+ typescript:
+ specifier: 'catalog:'
+ version: 5.9.3
+
+ benchmarks/usercentrics:
+ dependencies:
+ next:
+ specifier: 'catalog:'
+ version: 16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ react:
+ specifier: 'catalog:'
+ version: 19.2.4
+ react-dom:
+ specifier: 'catalog:'
+ version: 19.2.4(react@19.2.4)
+ devDependencies:
+ '@cookiebench/benchmark-schema':
+ specifier: workspace:*
+ version: link:../../packages/benchmark-schema
'@cookiebench/ts-config':
specifier: workspace:*
version: link:../../packages/typescript-config
'@types/node':
- specifier: ^24.10.1
- version: 24.10.1
+ specifier: 'catalog:'
+ version: 25.2.3
'@types/react':
- specifier: ^19.2.7
- version: 19.2.7
+ specifier: 'catalog:'
+ version: 19.2.14
'@types/react-dom':
- specifier: ^19.2.3
- version: 19.2.3(@types/react@19.2.7)
+ specifier: 'catalog:'
+ version: 19.2.3(@types/react@19.2.14)
typescript:
- specifier: ^5.9.3
+ specifier: 'catalog:'
version: 5.9.3
benchmarks/with-c15t-nextjs:
dependencies:
'@c15t/nextjs':
specifier: 1.8.1
- version: 1.8.1(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(@opentelemetry/instrumentation@0.208.0(@opentelemetry/api@1.9.0))(@types/better-sqlite3@7.6.13)(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(better-sqlite3@12.5.0)(next@16.0.7(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typeorm@0.3.28(better-sqlite3@12.5.0))(use-sync-external-store@1.6.0(react@19.2.1))(ws@8.18.3)
+ version: 1.8.1(@opentelemetry/api@1.9.0)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(next@16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(ws@8.18.3)
next:
- specifier: 16.0.7
- version: 16.0.7(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+ specifier: 'catalog:'
+ version: 16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react:
- specifier: ^19.2.1
- version: 19.2.1
+ specifier: 'catalog:'
+ version: 19.2.4
react-dom:
- specifier: ^19.2.1
- version: 19.2.1(react@19.2.1)
+ specifier: 'catalog:'
+ version: 19.2.4(react@19.2.4)
devDependencies:
'@cookiebench/benchmark-schema':
specifier: workspace:*
@@ -101,35 +472,35 @@ importers:
specifier: workspace:*
version: link:../../packages/typescript-config
'@types/node':
- specifier: ^24.10.1
- version: 24.10.1
+ specifier: 'catalog:'
+ version: 25.2.3
'@types/react':
- specifier: ^19.2.7
- version: 19.2.7
+ specifier: 'catalog:'
+ version: 19.2.14
'@types/react-dom':
- specifier: ^19.2.3
- version: 19.2.3(@types/react@19.2.7)
+ specifier: 'catalog:'
+ version: 19.2.3(@types/react@19.2.14)
typescript:
- specifier: ^5.9.3
+ specifier: 'catalog:'
version: 5.9.3
benchmarks/with-c15t-react:
dependencies:
'@c15t/react':
specifier: 1.8.1
- version: 1.8.1(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(@opentelemetry/instrumentation@0.208.0(@opentelemetry/api@1.9.0))(@types/better-sqlite3@7.6.13)(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(better-sqlite3@12.5.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typeorm@0.3.28(better-sqlite3@12.5.0))(use-sync-external-store@1.6.0(react@19.2.1))(ws@8.18.3)
+ version: 1.8.1(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(@opentelemetry/instrumentation@0.207.0(@opentelemetry/api@1.9.0))(@types/better-sqlite3@7.6.13)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(better-sqlite3@12.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typeorm@0.3.27(better-sqlite3@12.4.1)(reflect-metadata@0.2.2))(use-sync-external-store@1.6.0(react@19.2.4))(ws@8.18.3)
'@c15t/translations':
specifier: ^1.8.0
version: 1.8.0
next:
- specifier: 16.0.7
- version: 16.0.7(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+ specifier: 'catalog:'
+ version: 16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react:
- specifier: ^19.2.1
- version: 19.2.1
+ specifier: 'catalog:'
+ version: 19.2.4
react-dom:
- specifier: ^19.2.1
- version: 19.2.1(react@19.2.1)
+ specifier: 'catalog:'
+ version: 19.2.4(react@19.2.4)
devDependencies:
'@cookiebench/benchmark-schema':
specifier: workspace:*
@@ -141,29 +512,29 @@ importers:
specifier: workspace:*
version: link:../../packages/typescript-config
'@types/node':
- specifier: ^24.10.1
- version: 24.10.1
+ specifier: 'catalog:'
+ version: 25.2.3
'@types/react':
- specifier: ^19.2.7
- version: 19.2.7
+ specifier: 'catalog:'
+ version: 19.2.14
'@types/react-dom':
- specifier: ^19.2.3
- version: 19.2.3(@types/react@19.2.7)
+ specifier: 'catalog:'
+ version: 19.2.3(@types/react@19.2.14)
typescript:
- specifier: ^5.9.3
+ specifier: 'catalog:'
version: 5.9.3
benchmarks/with-cookie-control:
dependencies:
next:
- specifier: 16.0.7
- version: 16.0.7(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+ specifier: 'catalog:'
+ version: 16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react:
- specifier: ^19.2.1
- version: 19.2.1
+ specifier: 'catalog:'
+ version: 19.2.4
react-dom:
- specifier: ^19.2.1
- version: 19.2.1(react@19.2.1)
+ specifier: 'catalog:'
+ version: 19.2.4(react@19.2.4)
devDependencies:
'@cookiebench/benchmark-schema':
specifier: workspace:*
@@ -175,29 +546,29 @@ importers:
specifier: workspace:*
version: link:../../packages/typescript-config
'@types/node':
- specifier: ^24.10.1
- version: 24.10.1
+ specifier: 'catalog:'
+ version: 25.2.3
'@types/react':
- specifier: ^19.2.7
- version: 19.2.7
+ specifier: 'catalog:'
+ version: 19.2.14
'@types/react-dom':
- specifier: ^19.2.3
- version: 19.2.3(@types/react@19.2.7)
+ specifier: 'catalog:'
+ version: 19.2.3(@types/react@19.2.14)
typescript:
- specifier: ^5.9.3
+ specifier: 'catalog:'
version: 5.9.3
benchmarks/with-cookie-yes:
dependencies:
next:
- specifier: 16.0.7
- version: 16.0.7(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+ specifier: 'catalog:'
+ version: 16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react:
- specifier: ^19.2.1
- version: 19.2.1
+ specifier: 'catalog:'
+ version: 19.2.4
react-dom:
- specifier: ^19.2.1
- version: 19.2.1(react@19.2.1)
+ specifier: 'catalog:'
+ version: 19.2.4(react@19.2.4)
devDependencies:
'@cookiebench/benchmark-schema':
specifier: workspace:*
@@ -209,32 +580,32 @@ importers:
specifier: workspace:*
version: link:../../packages/typescript-config
'@types/node':
- specifier: ^24.10.1
- version: 24.10.1
+ specifier: 'catalog:'
+ version: 25.2.3
'@types/react':
- specifier: ^19.2.7
- version: 19.2.7
+ specifier: 'catalog:'
+ version: 19.2.14
'@types/react-dom':
- specifier: ^19.2.3
- version: 19.2.3(@types/react@19.2.7)
+ specifier: 'catalog:'
+ version: 19.2.3(@types/react@19.2.14)
typescript:
- specifier: ^5.9.3
+ specifier: 'catalog:'
version: 5.9.3
benchmarks/with-didomi:
dependencies:
'@didomi/react':
specifier: ^1.8.8
- version: 1.8.8(react@19.2.1)
+ version: 1.8.8(react@19.2.4)
next:
- specifier: 16.0.7
- version: 16.0.7(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+ specifier: 'catalog:'
+ version: 16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react:
- specifier: ^19.2.1
- version: 19.2.1
+ specifier: 'catalog:'
+ version: 19.2.4
react-dom:
- specifier: ^19.2.1
- version: 19.2.1(react@19.2.1)
+ specifier: 'catalog:'
+ version: 19.2.4(react@19.2.4)
devDependencies:
'@cookiebench/benchmark-schema':
specifier: workspace:*
@@ -243,29 +614,29 @@ importers:
specifier: workspace:*
version: link:../../packages/typescript-config
'@types/node':
- specifier: ^24.10.1
- version: 24.10.1
+ specifier: 'catalog:'
+ version: 25.2.3
'@types/react':
- specifier: ^19.2.7
- version: 19.2.7
+ specifier: 'catalog:'
+ version: 19.2.14
'@types/react-dom':
- specifier: ^19.2.3
- version: 19.2.3(@types/react@19.2.7)
+ specifier: 'catalog:'
+ version: 19.2.3(@types/react@19.2.14)
typescript:
- specifier: ^5.9.3
+ specifier: 'catalog:'
version: 5.9.3
benchmarks/with-enzuzo:
dependencies:
next:
- specifier: 16.0.7
- version: 16.0.7(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+ specifier: 'catalog:'
+ version: 16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react:
- specifier: ^19.2.1
- version: 19.2.1
+ specifier: 'catalog:'
+ version: 19.2.4
react-dom:
- specifier: ^19.2.1
- version: 19.2.1(react@19.2.1)
+ specifier: 'catalog:'
+ version: 19.2.4(react@19.2.4)
devDependencies:
'@cookiebench/benchmark-schema':
specifier: workspace:*
@@ -277,29 +648,29 @@ importers:
specifier: workspace:*
version: link:../../packages/typescript-config
'@types/node':
- specifier: ^24.10.1
- version: 24.10.1
+ specifier: 'catalog:'
+ version: 25.2.3
'@types/react':
- specifier: ^19.2.7
- version: 19.2.7
+ specifier: 'catalog:'
+ version: 19.2.14
'@types/react-dom':
- specifier: ^19.2.3
- version: 19.2.3(@types/react@19.2.7)
+ specifier: 'catalog:'
+ version: 19.2.3(@types/react@19.2.14)
typescript:
- specifier: ^5.9.3
+ specifier: 'catalog:'
version: 5.9.3
benchmarks/with-iubenda:
dependencies:
next:
- specifier: 16.0.7
- version: 16.0.7(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+ specifier: 'catalog:'
+ version: 16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react:
- specifier: ^19.2.1
- version: 19.2.1
+ specifier: 'catalog:'
+ version: 19.2.4
react-dom:
- specifier: ^19.2.1
- version: 19.2.1(react@19.2.1)
+ specifier: 'catalog:'
+ version: 19.2.4(react@19.2.4)
devDependencies:
'@cookiebench/benchmark-schema':
specifier: workspace:*
@@ -311,29 +682,29 @@ importers:
specifier: workspace:*
version: link:../../packages/typescript-config
'@types/node':
- specifier: ^24.10.1
- version: 24.10.1
+ specifier: 'catalog:'
+ version: 25.2.3
'@types/react':
- specifier: ^19.2.7
- version: 19.2.7
+ specifier: 'catalog:'
+ version: 19.2.14
'@types/react-dom':
- specifier: ^19.2.3
- version: 19.2.3(@types/react@19.2.7)
+ specifier: 'catalog:'
+ version: 19.2.3(@types/react@19.2.14)
typescript:
- specifier: ^5.9.3
+ specifier: 'catalog:'
version: 5.9.3
benchmarks/with-ketch:
dependencies:
next:
- specifier: 16.0.7
- version: 16.0.7(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+ specifier: 'catalog:'
+ version: 16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react:
- specifier: ^19.2.1
- version: 19.2.1
+ specifier: 'catalog:'
+ version: 19.2.4
react-dom:
- specifier: ^19.2.1
- version: 19.2.1(react@19.2.1)
+ specifier: 'catalog:'
+ version: 19.2.4(react@19.2.4)
devDependencies:
'@cookiebench/benchmark-schema':
specifier: workspace:*
@@ -342,29 +713,29 @@ importers:
specifier: workspace:*
version: link:../../packages/typescript-config
'@types/node':
- specifier: ^24.10.1
- version: 24.10.1
+ specifier: 'catalog:'
+ version: 25.2.3
'@types/react':
- specifier: ^19.2.7
- version: 19.2.7
+ specifier: 'catalog:'
+ version: 19.2.14
'@types/react-dom':
- specifier: ^19.2.3
- version: 19.2.3(@types/react@19.2.7)
+ specifier: 'catalog:'
+ version: 19.2.3(@types/react@19.2.14)
typescript:
- specifier: ^5.9.3
+ specifier: 'catalog:'
version: 5.9.3
benchmarks/with-onetrust:
dependencies:
next:
- specifier: 16.0.7
- version: 16.0.7(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+ specifier: 'catalog:'
+ version: 16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react:
- specifier: ^19.2.1
- version: 19.2.1
+ specifier: 'catalog:'
+ version: 19.2.4
react-dom:
- specifier: ^19.2.1
- version: 19.2.1(react@19.2.1)
+ specifier: 'catalog:'
+ version: 19.2.4(react@19.2.4)
devDependencies:
'@cookiebench/benchmark-schema':
specifier: workspace:*
@@ -376,29 +747,29 @@ importers:
specifier: workspace:*
version: link:../../packages/typescript-config
'@types/node':
- specifier: ^24.10.1
- version: 24.10.1
+ specifier: 'catalog:'
+ version: 25.2.3
'@types/react':
- specifier: ^19.2.7
- version: 19.2.7
+ specifier: 'catalog:'
+ version: 19.2.14
'@types/react-dom':
- specifier: ^19.2.3
- version: 19.2.3(@types/react@19.2.7)
+ specifier: 'catalog:'
+ version: 19.2.3(@types/react@19.2.14)
typescript:
- specifier: ^5.9.3
+ specifier: 'catalog:'
version: 5.9.3
benchmarks/with-osano:
dependencies:
next:
- specifier: 16.0.7
- version: 16.0.7(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+ specifier: 'catalog:'
+ version: 16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react:
- specifier: ^19.2.1
- version: 19.2.1
+ specifier: 'catalog:'
+ version: 19.2.4
react-dom:
- specifier: ^19.2.1
- version: 19.2.1(react@19.2.1)
+ specifier: 'catalog:'
+ version: 19.2.4(react@19.2.4)
devDependencies:
'@cookiebench/benchmark-schema':
specifier: workspace:*
@@ -410,29 +781,29 @@ importers:
specifier: workspace:*
version: link:../../packages/typescript-config
'@types/node':
- specifier: ^24.10.1
- version: 24.10.1
+ specifier: 'catalog:'
+ version: 25.2.3
'@types/react':
- specifier: ^19.2.7
- version: 19.2.7
+ specifier: 'catalog:'
+ version: 19.2.14
'@types/react-dom':
- specifier: ^19.2.3
- version: 19.2.3(@types/react@19.2.7)
+ specifier: 'catalog:'
+ version: 19.2.3(@types/react@19.2.14)
typescript:
- specifier: ^5.9.3
+ specifier: 'catalog:'
version: 5.9.3
benchmarks/with-usercentrics:
dependencies:
next:
- specifier: 16.0.7
- version: 16.0.7(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
+ specifier: 'catalog:'
+ version: 16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react:
- specifier: ^19.2.1
- version: 19.2.1
+ specifier: 'catalog:'
+ version: 19.2.4
react-dom:
- specifier: ^19.2.1
- version: 19.2.1(react@19.2.1)
+ specifier: 'catalog:'
+ version: 19.2.4(react@19.2.4)
devDependencies:
'@cookiebench/benchmark-schema':
specifier: workspace:*
@@ -444,16 +815,47 @@ importers:
specifier: workspace:*
version: link:../../packages/typescript-config
'@types/node':
- specifier: ^24.10.1
- version: 24.10.1
+ specifier: 'catalog:'
+ version: 25.2.3
'@types/react':
- specifier: ^19.2.7
- version: 19.2.7
+ specifier: 'catalog:'
+ version: 19.2.14
'@types/react-dom':
- specifier: ^19.2.3
- version: 19.2.3(@types/react@19.2.7)
+ specifier: 'catalog:'
+ version: 19.2.3(@types/react@19.2.14)
+ typescript:
+ specifier: 'catalog:'
+ version: 5.9.3
+
+ packages/benchmark:
+ dependencies:
+ '@c15t/logger':
+ specifier: ^1.0.1
+ version: 1.0.1
+ '@consentio/shared':
+ specifier: workspace:*
+ version: link:../shared
+ '@playwright/test':
+ specifier: ^1.58.2
+ version: 1.58.2
+ perfume.js:
+ specifier: ^9.4.0
+ version: 9.4.0
+ playwright-performance-metrics:
+ specifier: ^1.2.5
+ version: 1.2.5(@playwright/test@1.58.2)
+ devDependencies:
+ '@rsdoctor/rspack-plugin':
+ specifier: ^1.5.2
+ version: 1.5.2(@rsbuild/core@1.6.0-beta.1)(@rspack/core@1.7.6(@swc/helpers@0.5.18))
+ '@rslib/core':
+ specifier: ^0.16.1
+ version: 0.16.1(typescript@5.9.3)
+ '@types/node':
+ specifier: 'catalog:'
+ version: 25.2.3
typescript:
- specifier: ^5.9.3
+ specifier: 'catalog:'
version: 5.9.3
packages/benchmark-schema: {}
@@ -464,14 +866,14 @@ importers:
specifier: 1.0.0-alpha.6
version: 1.0.0-alpha.6
'@playwright/test':
- specifier: ^1.57.0
- version: 1.57.0
+ specifier: ^1.58.2
+ version: 1.58.2
cli-table3:
specifier: ^0.6.5
version: 0.6.5
dotenv:
- specifier: ^17.2.3
- version: 17.2.3
+ specifier: ^17.3.1
+ version: 17.3.1
package-manager-detector:
specifier: ^1.6.0
version: 1.6.0
@@ -480,19 +882,117 @@ importers:
version: 1.1.1
devDependencies:
'@rsdoctor/rspack-plugin':
- specifier: ^1.3.12
- version: 1.3.12(@rsbuild/core@1.6.12)(@rspack/core@1.6.6(@swc/helpers@0.5.17))
+ specifier: ^1.5.2
+ version: 1.5.2(@rsbuild/core@1.7.3)(@rspack/core@1.7.6(@swc/helpers@0.5.18))
+ '@rslib/core':
+ specifier: ^0.18.6
+ version: 0.18.6(typescript@5.9.3)
+ '@types/node':
+ specifier: 'catalog:'
+ version: 25.2.3
+ playwright-performance-metrics:
+ specifier: ^1.2.5
+ version: 1.2.5(@playwright/test@1.58.2)
+ typescript:
+ specifier: 'catalog:'
+ version: 5.9.3
+
+ packages/cookiebench-cli:
+ dependencies:
+ '@c15t/logger':
+ specifier: ^1.0.1
+ version: 1.0.1
+ '@clack/prompts':
+ specifier: ^1.0.1
+ version: 1.0.1
+ '@consentio/benchmark':
+ specifier: workspace:*
+ version: link:../benchmark
+ '@consentio/runner':
+ specifier: workspace:*
+ version: link:../runner
+ '@consentio/shared':
+ specifier: workspace:*
+ version: link:../shared
+ cli-table3:
+ specifier: ^0.6.5
+ version: 0.6.5
+ dotenv:
+ specifier: ^17.3.1
+ version: 17.3.1
+ figlet:
+ specifier: ^1.10.0
+ version: 1.10.0
+ picocolors:
+ specifier: ^1.1.1
+ version: 1.1.1
+ pretty-ms:
+ specifier: ^9.3.0
+ version: 9.3.0
+ devDependencies:
+ '@rsdoctor/rspack-plugin':
+ specifier: ^1.5.2
+ version: 1.5.2(@rsbuild/core@1.7.3)(@rspack/core@1.7.6(@swc/helpers@0.5.18))
'@rslib/core':
- specifier: ^0.18.3
- version: 0.18.3(typescript@5.9.3)
+ specifier: ^0.16.1
+ version: 0.16.1(typescript@5.9.3)
+ '@types/figlet':
+ specifier: ^1.7.0
+ version: 1.7.0
'@types/node':
- specifier: ^24.10.1
- version: 24.10.1
+ specifier: 'catalog:'
+ version: 25.2.3
+ typescript:
+ specifier: 'catalog:'
+ version: 5.9.3
+
+ packages/runner:
+ dependencies:
+ '@c15t/logger':
+ specifier: 1.0.0
+ version: 1.0.0
+ '@consentio/benchmark':
+ specifier: workspace:*
+ version: link:../benchmark
+ '@consentio/shared':
+ specifier: workspace:*
+ version: link:../shared
+ '@playwright/test':
+ specifier: ^1.58.2
+ version: 1.58.2
playwright-performance-metrics:
- specifier: ^1.2.4
- version: 1.2.4(@playwright/test@1.57.0)
+ specifier: ^1.2.5
+ version: 1.2.5(@playwright/test@1.58.2)
+ devDependencies:
+ '@rsdoctor/rspack-plugin':
+ specifier: ^1.5.2
+ version: 1.5.2(@rsbuild/core@1.7.3)(@rspack/core@1.7.6(@swc/helpers@0.5.18))
+ '@rslib/core':
+ specifier: ^0.16.1
+ version: 0.16.1(typescript@5.9.3)
+ '@types/node':
+ specifier: 'catalog:'
+ version: 25.2.3
+ typescript:
+ specifier: 'catalog:'
+ version: 5.9.3
+
+ packages/shared:
+ devDependencies:
+ '@cookiebench/ts-config':
+ specifier: workspace:*
+ version: link:../typescript-config
+ '@rsdoctor/rspack-plugin':
+ specifier: ^1.5.2
+ version: 1.5.2(@rsbuild/core@1.7.3)(@rspack/core@1.7.6(@swc/helpers@0.5.18))
+ '@rslib/core':
+ specifier: ^0.16.1
+ version: 0.16.1(typescript@5.9.3)
+ '@types/node':
+ specifier: ^24.0.9
+ version: 24.10.13
typescript:
- specifier: ^5.9.3
+ specifier: 'catalog:'
version: 5.9.3
packages/typescript-config: {}
@@ -516,24 +1016,28 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@ast-grep/napi-linux-arm64-musl@0.37.0':
resolution: {integrity: sha512-LF9sAvYy6es/OdyJDO3RwkX3I82Vkfsng1sqUBcoWC1jVb1wX5YVzHtpQox9JrEhGl+bNp7FYxB4Qba9OdA5GA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@ast-grep/napi-linux-x64-gnu@0.37.0':
resolution: {integrity: sha512-TViz5/klqre6aSmJzswEIjApnGjJzstG/SE8VDWsrftMBMYt2PTu3MeluZVwzSqDao8doT/P+6U11dU05UOgxw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@ast-grep/napi-linux-x64-musl@0.37.0':
resolution: {integrity: sha512-/BcCH33S9E3ovOAEoxYngUNXgb+JLg991sdyiNP2bSoYd30a9RHrG7CYwW6fMgua3ijQ474eV6cq9yZO1bCpXg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@ast-grep/napi-win32-arm64-msvc@0.37.0':
resolution: {integrity: sha512-TjQA4cFoIEW2bgjLkaL9yqT4XWuuLa5MCNd0VCDhGRDMNQ9+rhwi9eLOWRaap3xzT7g+nlbcEHL3AkVCD2+b3A==}
@@ -565,65 +1069,82 @@ packages:
resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
engines: {node: '>=6.9.0'}
- '@biomejs/biome@2.3.8':
- resolution: {integrity: sha512-Qjsgoe6FEBxWAUzwFGFrB+1+M8y/y5kwmg5CHac+GSVOdmOIqsAiXM5QMVGZJ1eCUCLlPZtq4aFAQ0eawEUuUA==}
+ '@biomejs/biome@2.3.2':
+ resolution: {integrity: sha512-8e9tzamuDycx7fdrcJ/F/GDZ8SYukc5ud6tDicjjFqURKYFSWMl0H0iXNXZEGmcmNUmABgGuHThPykcM41INgg==}
engines: {node: '>=14.21.3'}
hasBin: true
- '@biomejs/cli-darwin-arm64@2.3.8':
- resolution: {integrity: sha512-HM4Zg9CGQ3txTPflxD19n8MFPrmUAjaC7PQdLkugeeC0cQ+PiVrd7i09gaBS/11QKsTDBJhVg85CEIK9f50Qww==}
+ '@biomejs/cli-darwin-arm64@2.3.2':
+ resolution: {integrity: sha512-4LECm4kc3If0JISai4c3KWQzukoUdpxy4fRzlrPcrdMSRFksR9ZoXK7JBcPuLBmd2SoT4/d7CQS33VnZpgBjew==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [darwin]
- '@biomejs/cli-darwin-x64@2.3.8':
- resolution: {integrity: sha512-lUDQ03D7y/qEao7RgdjWVGCu+BLYadhKTm40HkpJIi6kn8LSv5PAwRlew/DmwP4YZ9ke9XXoTIQDO1vAnbRZlA==}
+ '@biomejs/cli-darwin-x64@2.3.2':
+ resolution: {integrity: sha512-jNMnfwHT4N3wi+ypRfMTjLGnDmKYGzxVr1EYAPBcauRcDnICFXN81wD6wxJcSUrLynoyyYCdfW6vJHS/IAoTDA==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [darwin]
- '@biomejs/cli-linux-arm64-musl@2.3.8':
- resolution: {integrity: sha512-PShR4mM0sjksUMyxbyPNMxoKFPVF48fU8Qe8Sfx6w6F42verbwRLbz+QiKNiDPRJwUoMG1nPM50OBL3aOnTevA==}
+ '@biomejs/cli-linux-arm64-musl@2.3.2':
+ resolution: {integrity: sha512-2Zz4usDG1GTTPQnliIeNx6eVGGP2ry5vE/v39nT73a3cKN6t5H5XxjcEoZZh62uVZvED7hXXikclvI64vZkYqw==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
- '@biomejs/cli-linux-arm64@2.3.8':
- resolution: {integrity: sha512-Uo1OJnIkJgSgF+USx970fsM/drtPcQ39I+JO+Fjsaa9ZdCN1oysQmy6oAGbyESlouz+rzEckLTF6DS7cWse95g==}
+ '@biomejs/cli-linux-arm64@2.3.2':
+ resolution: {integrity: sha512-amnqvk+gWybbQleRRq8TMe0rIv7GHss8mFJEaGuEZYWg1Tw14YKOkeo8h6pf1c+d3qR+JU4iT9KXnBKGON4klw==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
- '@biomejs/cli-linux-x64-musl@2.3.8':
- resolution: {integrity: sha512-YGLkqU91r1276uwSjiUD/xaVikdxgV1QpsicT0bIA1TaieM6E5ibMZeSyjQ/izBn4tKQthUSsVZacmoJfa3pDA==}
+ '@biomejs/cli-linux-x64-musl@2.3.2':
+ resolution: {integrity: sha512-gzB19MpRdTuOuLtPpFBGrV3Lq424gHyq2lFj8wfX9tvLMLdmA/R9C7k/mqBp/spcbWuHeIEKgEs3RviOPcWGBA==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
+ libc: [musl]
- '@biomejs/cli-linux-x64@2.3.8':
- resolution: {integrity: sha512-QDPMD5bQz6qOVb3kiBui0zKZXASLo0NIQ9JVJio5RveBEFgDgsvJFUvZIbMbUZT3T00M/1wdzwWXk4GIh0KaAw==}
+ '@biomejs/cli-linux-x64@2.3.2':
+ resolution: {integrity: sha512-8BG/vRAhFz1pmuyd24FQPhNeueLqPtwvZk6yblABY2gzL2H8fLQAF/Z2OPIc+BPIVPld+8cSiKY/KFh6k81xfA==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
- '@biomejs/cli-win32-arm64@2.3.8':
- resolution: {integrity: sha512-H4IoCHvL1fXKDrTALeTKMiE7GGWFAraDwBYFquE/L/5r1927Te0mYIGseXi4F+lrrwhSWbSGt5qPFswNoBaCxg==}
+ '@biomejs/cli-win32-arm64@2.3.2':
+ resolution: {integrity: sha512-lCruqQlfWjhMlOdyf5pDHOxoNm4WoyY2vZ4YN33/nuZBRstVDuqPPjS0yBkbUlLEte11FbpW+wWSlfnZfSIZvg==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [win32]
- '@biomejs/cli-win32-x64@2.3.8':
- resolution: {integrity: sha512-RguzimPoZWtBapfKhKjcWXBVI91tiSprqdBYu7tWhgN8pKRZhw24rFeNZTNf6UiBfjCYCi9eFQs/JzJZIhuK4w==}
+ '@biomejs/cli-win32-x64@2.3.2':
+ resolution: {integrity: sha512-6Ee9P26DTb4D8sN9nXxgbi9Dw5vSOfH98M7UlmkjKB2vtUbrRqCbZiNfryGiwnPIpd6YUoTl7rLVD2/x1CyEHQ==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [win32]
+ '@c15t/backend@1.7.1':
+ resolution: {integrity: sha512-oO+UM7fI93qb1KC+r02FCPeva+cUEySZ0S3MPbUWM5OG5vc/tz6JaPeAK9wEc1TTXszvv5zuNKrK1FYgYNcNdQ==}
+
'@c15t/backend@1.8.0':
resolution: {integrity: sha512-vM/jXQfWrI2XqN8h3Wq0IRWFBLz5o98vPUVAPu8Mdw9WblnBRwVRIEtmN6maMVM3yyai4yZA8fbXAi0e3PSM/Q==}
+ '@c15t/logger@1.0.0':
+ resolution: {integrity: sha512-z2RrUnvO5bbEg/qd9iD/TqdBi04vfvALf2uovDVLJiQTNfOVUb7FM/GkPH5mugiDwErlrtVhAW8hW/2VmBXbUA==}
+
'@c15t/logger@1.0.1':
resolution: {integrity: sha512-Yz+HkHjsGWPkJ7nWGqlSgF3LG9WtV72Om5NoWrW6YJHLMNasC5Z6opi9GM8B1Nm8ivZBzJpADEc4gVIBCkqamw==}
+ '@c15t/nextjs@1.7.1':
+ resolution: {integrity: sha512-TNs9x5eKQ+vkJ9LY0h54DyGZqa4GeUMqnTSS5PC7hczA/QkyoUJkyj7agRUzZsPVBeQEVw7xXbJEhuwr8p+C5Q==}
+ peerDependencies:
+ next: ^15.0.0 || ^14.0.0 || ^13.0.0
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+
'@c15t/nextjs@1.8.1':
resolution: {integrity: sha512-4Y9KShd/jE2wpz6/5cNURzuBDDUcy9XX13b/oYtzzwLa5Qy3fg+O/2AhwyM2hWok83n3WygS7xWex1YUyYzroQ==}
peerDependencies:
@@ -631,12 +1152,21 @@ packages:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ '@c15t/react@1.7.1':
+ resolution: {integrity: sha512-Ey6SUOxhf3nEiomYhRR9Axiy4Ku47zrSIDIvJ+amhHEhCS8jyJNQBdclUAggUFzvB8xVJQfGuniU6X6IzjL39Q==}
+ peerDependencies:
+ react: ^19.0.0 || ^19.0.0-rc || ^18.0.0 || ^17.0.0 || ^16.8.0
+ react-dom: ^19.0.0 || ^19.0.0-rc || ^18.0.0 || ^17.0.0 || ^16.8.0
+
'@c15t/react@1.8.1':
resolution: {integrity: sha512-Slh98Rxo4gYhrcOU+rR7M8HiAB084EE8VyW6JMXC8ItXWSCqu+1O89hpikn+2i3PdgyCIbwUv+VKEVm4KcAmBg==}
peerDependencies:
react: ^19.0.0 || ^19.0.0-rc || ^18.0.0 || ^17.0.0 || ^16.8.0
react-dom: ^19.0.0 || ^19.0.0-rc || ^18.0.0 || ^17.0.0 || ^16.8.0
+ '@c15t/translations@1.7.0':
+ resolution: {integrity: sha512-irrJAni2Cei56wHLmGoIoPHoXk/ygXllWs4dBF9LL0D/Ns7bvcBi/CAPCnCD1hw4FnoJ9amPqYtVKQ3sT++JaQ==}
+
'@c15t/translations@1.8.0':
resolution: {integrity: sha512-iCw8iOc9lgXpIJaN/WzN+T3FMzFUgjN+UZ7tp6hKSV4RktXwr7C/z3HAOdeGYZE01BdmunrlOZGQB/N3hZoZZQ==}
@@ -646,12 +1176,18 @@ packages:
'@clack/core@1.0.0-alpha.6':
resolution: {integrity: sha512-eG5P45+oShFG17u9I1DJzLkXYB1hpUgTLi32EfsMjSHLEqJUR8BOBCVFkdbUX2g08eh/HCi6UxNGpPhaac1LAA==}
+ '@clack/core@1.0.1':
+ resolution: {integrity: sha512-WKeyK3NOBwDOzagPR5H08rFk9D/WuN705yEbuZvKqlkmoLM2woKtXb10OO2k1NoSU4SFG947i2/SCYh+2u5e4g==}
+
'@clack/prompts@0.11.0':
resolution: {integrity: sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==}
'@clack/prompts@1.0.0-alpha.6':
resolution: {integrity: sha512-75NCtYOgDHVBE2nLdKPTDYOaESxO0GLAKC7INREp5VbS988Xua1u+588VaGlcvXiLc/kSwc25Cd+4PeTSpY6QQ==}
+ '@clack/prompts@1.0.1':
+ resolution: {integrity: sha512-/42G73JkuYdyWZ6m8d/CJtBrGl1Hegyc7Fy78m5Ob+jF85TOUmLR5XLce/U3LxYAw0kJ8CT5aI99RIvPHcGp/Q==}
+
'@colors/colors@1.5.0':
resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==}
engines: {node: '>=0.1.90'}
@@ -664,11 +1200,11 @@ packages:
'@drizzle-team/brocli@0.10.2':
resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==}
- '@emnapi/core@1.7.1':
- resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==}
+ '@emnapi/core@1.8.1':
+ resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==}
- '@emnapi/runtime@1.7.1':
- resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==}
+ '@emnapi/runtime@1.8.1':
+ resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==}
'@emnapi/wasi-threads@1.1.0':
resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==}
@@ -969,8 +1505,8 @@ packages:
cpu: [x64]
os: [win32]
- '@grpc/grpc-js@1.14.2':
- resolution: {integrity: sha512-QzVUtEFyu05UNx2xr0fCQmStUO17uVQhGNowtxs00IgTZT6/W2PBLfUkj30s0FKJ29VtTa3ArVNIhNP6akQhqA==}
+ '@grpc/grpc-js@1.14.3':
+ resolution: {integrity: sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==}
engines: {node: '>=12.10.0'}
'@grpc/proto-loader@0.8.0':
@@ -1008,89 +1544,105 @@ packages:
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-arm@1.2.4':
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
cpu: [arm]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-ppc64@1.2.4':
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-riscv64@1.2.4':
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.2.4':
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-x64@1.2.4':
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@img/sharp-linux-arm64@0.34.5':
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-arm@0.34.5':
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-ppc64@0.34.5':
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-riscv64@0.34.5':
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-s390x@0.34.5':
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-x64@0.34.5':
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.34.5':
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@img/sharp-linuxmusl-x64@0.34.5':
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@img/sharp-wasm32@0.34.5':
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
@@ -1115,14 +1667,6 @@ packages:
cpu: [x64]
os: [win32]
- '@isaacs/balanced-match@4.0.1':
- resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
- engines: {node: 20 || >=22}
-
- '@isaacs/brace-expansion@5.0.0':
- resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==}
- engines: {node: 20 || >=22}
-
'@isaacs/cliui@8.0.2':
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
@@ -1191,23 +1735,41 @@ packages:
cpu: [x64]
os: [win32]
- '@module-federation/error-codes@0.21.6':
- resolution: {integrity: sha512-MLJUCQ05KnoVl8xd6xs9a5g2/8U+eWmVxg7xiBMeR0+7OjdWUbHwcwgVFatRIwSZvFgKHfWEiI7wsU1q1XbTRQ==}
+ '@module-federation/error-codes@0.21.1':
+ resolution: {integrity: sha512-h1brnwR9AbwMu1P7ZoJJ9j2O2XWkuMh5p03WhXI1vNEdl3xJheSAvH8RjG8FoKRccVgMnUNDQ+vDVwevUBms/A==}
+
+ '@module-federation/error-codes@0.22.0':
+ resolution: {integrity: sha512-xF9SjnEy7vTdx+xekjPCV5cIHOGCkdn3pIxo9vU7gEZMIw0SvAEdsy6Uh17xaCpm8V0FWvR0SZoK9Ik6jGOaug==}
+
+ '@module-federation/runtime-core@0.21.1':
+ resolution: {integrity: sha512-COob5bepqDc9mKjTziXbQd4WQMCTzhc0cuXyraZhYddYcjcepzZrMpDIXG1x5p+gdg5p1vsGNWt/ZcU8cFh/pg==}
- '@module-federation/runtime-core@0.21.6':
- resolution: {integrity: sha512-5Hd1Y5qp5lU/aTiK66lidMlM/4ji2gr3EXAtJdreJzkY+bKcI5+21GRcliZ4RAkICmvdxQU5PHPL71XmNc7Lsw==}
+ '@module-federation/runtime-core@0.22.0':
+ resolution: {integrity: sha512-GR1TcD6/s7zqItfhC87zAp30PqzvceoeDGYTgF3Vx2TXvsfDrhP6Qw9T4vudDQL3uJRne6t7CzdT29YyVxlgIA==}
- '@module-federation/runtime-tools@0.21.6':
- resolution: {integrity: sha512-fnP+ZOZTFeBGiTAnxve+axGmiYn2D60h86nUISXjXClK3LUY1krUfPgf6MaD4YDJ4i51OGXZWPekeMe16pkd8Q==}
+ '@module-federation/runtime-tools@0.21.1':
+ resolution: {integrity: sha512-uQmammw3Osg8370yiRqZwKo7eA5zkyml9pAX9x4oS9QAkEBvQpDogERlF9f7gAgcP2P3v+xLg3/bCdquD0gt8A==}
- '@module-federation/runtime@0.21.6':
- resolution: {integrity: sha512-+caXwaQqwTNh+CQqyb4mZmXq7iEemRDrTZQGD+zyeH454JAYnJ3s/3oDFizdH6245pk+NiqDyOOkHzzFQorKhQ==}
+ '@module-federation/runtime-tools@0.22.0':
+ resolution: {integrity: sha512-4ScUJ/aUfEernb+4PbLdhM/c60VHl698Gn1gY21m9vyC1Ucn69fPCA1y2EwcCB7IItseRMoNhdcWQnzt/OPCNA==}
- '@module-federation/sdk@0.21.6':
- resolution: {integrity: sha512-x6hARETb8iqHVhEsQBysuWpznNZViUh84qV2yE7AD+g7uIzHKiYdoWqj10posbo5XKf/147qgWDzKZoKoEP2dw==}
+ '@module-federation/runtime@0.21.1':
+ resolution: {integrity: sha512-sfBrP0gEPwXPEiREVKVd0IjEWXtr3G/i7EUZVWTt4D491nNpswog/kuKFatGmhcBb+9uD5v9rxFgmIbgL9njnQ==}
- '@module-federation/webpack-bundler-runtime@0.21.6':
- resolution: {integrity: sha512-7zIp3LrcWbhGuFDTUMLJ2FJvcwjlddqhWGxi/MW3ur1a+HaO8v5tF2nl+vElKmbG1DFLU/52l3PElVcWf/YcsQ==}
+ '@module-federation/runtime@0.22.0':
+ resolution: {integrity: sha512-38g5iPju2tPC3KHMPxRKmy4k4onNp6ypFPS1eKGsNLUkXgHsPMBFqAjDw96iEcjri91BrahG4XcdyKi97xZzlA==}
+
+ '@module-federation/sdk@0.21.1':
+ resolution: {integrity: sha512-1cHMrmCCao3NMFM4BkA0GDt4rbYbyneHct5E4z68cu5UBUnI3L/UboP5VNM8lkYMO1nCR8M0FcLkLhK35Nt48A==}
+
+ '@module-federation/sdk@0.22.0':
+ resolution: {integrity: sha512-x4aFNBKn2KVQRuNVC5A7SnrSCSqyfIWmm1DvubjbO9iKFe7ith5niw8dqSFBekYBg2Fwy+eMg4sEFNVvCAdo6g==}
+
+ '@module-federation/webpack-bundler-runtime@0.21.1':
+ resolution: {integrity: sha512-yyXX6ugTV07pMxMzAHt6/JDwblS3f1NDyUI7l44CyYgXpl2ItEEUs5aj5h/5xU1c9Px7M//KkY3qW+InW4tR/A==}
+
+ '@module-federation/webpack-bundler-runtime@0.22.0':
+ resolution: {integrity: sha512-aM8gCqXu+/4wBmJtVeMeeMN5guw3chf+2i6HajKtQv7SJfxV/f4IyNQJUeUQu9HfiAZHjqtMV5Lvq/Lvh8LdyA==}
'@napi-rs/wasm-runtime@1.0.7':
resolution: {integrity: sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==}
@@ -1215,53 +1777,57 @@ packages:
'@neon-rs/load@0.0.4':
resolution: {integrity: sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==}
- '@next/env@16.0.7':
- resolution: {integrity: sha512-gpaNgUh5nftFKRkRQGnVi5dpcYSKGcZZkQffZ172OrG/XkrnS7UBTQ648YY+8ME92cC4IojpI2LqTC8sTDhAaw==}
+ '@next/env@16.1.6':
+ resolution: {integrity: sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==}
- '@next/swc-darwin-arm64@16.0.7':
- resolution: {integrity: sha512-LlDtCYOEj/rfSnEn/Idi+j1QKHxY9BJFmxx7108A6D8K0SB+bNgfYQATPk/4LqOl4C0Wo3LACg2ie6s7xqMpJg==}
+ '@next/swc-darwin-arm64@16.1.6':
+ resolution: {integrity: sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
- '@next/swc-darwin-x64@16.0.7':
- resolution: {integrity: sha512-rtZ7BhnVvO1ICf3QzfW9H3aPz7GhBrnSIMZyr4Qy6boXF0b5E3QLs+cvJmg3PsTCG2M1PBoC+DANUi4wCOKXpA==}
+ '@next/swc-darwin-x64@16.1.6':
+ resolution: {integrity: sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
- '@next/swc-linux-arm64-gnu@16.0.7':
- resolution: {integrity: sha512-mloD5WcPIeIeeZqAIP5c2kdaTa6StwP4/2EGy1mUw8HiexSHGK/jcM7lFuS3u3i2zn+xH9+wXJs6njO7VrAqww==}
+ '@next/swc-linux-arm64-gnu@16.1.6':
+ resolution: {integrity: sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
- '@next/swc-linux-arm64-musl@16.0.7':
- resolution: {integrity: sha512-+ksWNrZrthisXuo9gd1XnjHRowCbMtl/YgMpbRvFeDEqEBd523YHPWpBuDjomod88U8Xliw5DHhekBC3EOOd9g==}
+ '@next/swc-linux-arm64-musl@16.1.6':
+ resolution: {integrity: sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
- '@next/swc-linux-x64-gnu@16.0.7':
- resolution: {integrity: sha512-4WtJU5cRDxpEE44Ana2Xro1284hnyVpBb62lIpU5k85D8xXxatT+rXxBgPkc7C1XwkZMWpK5rXLXTh9PFipWsA==}
+ '@next/swc-linux-x64-gnu@16.1.6':
+ resolution: {integrity: sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
- '@next/swc-linux-x64-musl@16.0.7':
- resolution: {integrity: sha512-HYlhqIP6kBPXalW2dbMTSuB4+8fe+j9juyxwfMwCe9kQPPeiyFn7NMjNfoFOfJ2eXkeQsoUGXg+O2SE3m4Qg2w==}
+ '@next/swc-linux-x64-musl@16.1.6':
+ resolution: {integrity: sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
+ libc: [musl]
- '@next/swc-win32-arm64-msvc@16.0.7':
- resolution: {integrity: sha512-EviG+43iOoBRZg9deGauXExjRphhuYmIOJ12b9sAPy0eQ6iwcPxfED2asb/s2/yiLYOdm37kPaiZu8uXSYPs0Q==}
+ '@next/swc-win32-arm64-msvc@16.1.6':
+ resolution: {integrity: sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
- '@next/swc-win32-x64-msvc@16.0.7':
- resolution: {integrity: sha512-gniPjy55zp5Eg0896qSrf3yB1dw4F/3s8VK1ephdsZZ129j2n6e1WqCbE2YgcKhW9hPB9TVZENugquWJD5x0ug==}
+ '@next/swc-win32-x64-msvc@16.1.6':
+ resolution: {integrity: sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@@ -1274,8 +1840,8 @@ packages:
resolution: {integrity: sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==}
engines: {node: '>=8.0.0'}
- '@opentelemetry/api-logs@0.208.0':
- resolution: {integrity: sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==}
+ '@opentelemetry/api-logs@0.207.0':
+ resolution: {integrity: sha512-lAb0jQRVyleQQGiuuvCOTDVspc14nx6XJjP4FspJ1sNARo3Regq4ZZbrc3rN4b1TYSuUCvgH+UXUPug4SLOqEQ==}
engines: {node: '>=8.0.0'}
'@opentelemetry/api@1.9.0':
@@ -1294,8 +1860,8 @@ packages:
peerDependencies:
'@opentelemetry/api': '>=1.0.0 <1.10.0'
- '@opentelemetry/core@2.2.0':
- resolution: {integrity: sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==}
+ '@opentelemetry/core@2.5.1':
+ resolution: {integrity: sha512-Dwlc+3HAZqpgTYq0MUyZABjFkcrKTePwuiFVLjahGD8cx3enqihmpAmdgNFO1R4m/sIe5afjJrA25Prqy4NXlA==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': '>=1.0.0 <1.10.0'
@@ -1372,8 +1938,8 @@ packages:
peerDependencies:
'@opentelemetry/api': ^1.3.0
- '@opentelemetry/instrumentation@0.208.0':
- resolution: {integrity: sha512-Eju0L4qWcQS+oXxi6pgh7zvE2byogAkcsVv0OjHF/97iOz1N/aKE6etSGowYkie+YA1uo6DNwdSxaaNnLvcRlA==}
+ '@opentelemetry/instrumentation@0.207.0':
+ resolution: {integrity: sha512-y6eeli9+TLKnznrR8AZlQMSJT7wILpXH+6EYq5Vf/4Ao+huI7EedxQHwRgVUOMLFbe7VFDvHJrX9/f4lcwnJsA==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': ^1.3.0
@@ -1414,8 +1980,8 @@ packages:
peerDependencies:
'@opentelemetry/api': '>=1.3.0 <1.10.0'
- '@opentelemetry/resources@2.2.0':
- resolution: {integrity: sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==}
+ '@opentelemetry/resources@2.5.1':
+ resolution: {integrity: sha512-BViBCdE/GuXRlp9k7nS1w6wJvY5fnFX5XvuEtWsTAOQFIO89Eru7lGW3WbfbxtCuZ/GbrJfAziXG0w0dpxL7eQ==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': '>=1.3.0 <1.10.0'
@@ -1444,8 +2010,8 @@ packages:
peerDependencies:
'@opentelemetry/api': '>=1.3.0 <1.10.0'
- '@opentelemetry/sdk-trace-base@2.2.0':
- resolution: {integrity: sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==}
+ '@opentelemetry/sdk-trace-base@2.5.1':
+ resolution: {integrity: sha512-iZH3Gw8cxQn0gjpOjJMmKLd9GIaNh/E3v3ST67vyzLSxHBs14HsG4dy7jMYyC5WXGdBVEcM7U/XTF5hCQxjDMw==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': '>=1.3.0 <1.10.0'
@@ -1456,25 +2022,16 @@ packages:
peerDependencies:
'@opentelemetry/api': '>=1.0.0 <1.10.0'
- '@opentelemetry/semantic-conventions@1.38.0':
- resolution: {integrity: sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==}
+ '@opentelemetry/semantic-conventions@1.39.0':
+ resolution: {integrity: sha512-R5R9tb2AXs2IRLNKLBJDynhkfmx7mX0vi8NkhZb3gUkPWHn6HXk5J8iQ/dql0U3ApfWym4kXXmBDRGO+oeOfjg==}
engines: {node: '>=14'}
- '@orpc/client@1.12.2':
- resolution: {integrity: sha512-3MTFnWRYYjcyzhtcYpodvkaYQlqsxKd5xGv+7PPJSpjCgFg9wcp7mZmRKy7hK0sCwUlkyi7AKs1Q19aUVUFIGA==}
-
'@orpc/client@1.8.1':
resolution: {integrity: sha512-ewofUqn46yHQxnAOFLwZspQD1k3DPTUCnkqP6aFO1alUMHgLFNb03BKd1rgwUXSWaxWltKD8Db07HNxgltdk4A==}
- '@orpc/contract@1.12.2':
- resolution: {integrity: sha512-eleSbF7WgfkWz+7jl1b9t3C3DWn127694+yEdR3j6EiBjb9mzHMIeOMRTXsclIP4gWj13wD1NtXp1Qlv8m7oZw==}
-
'@orpc/contract@1.8.1':
resolution: {integrity: sha512-zw0xgRxOyR/otX+4UeGEm0VjuMH/AeVbEuWvPNRJC8xxvQpaPJFo8Nr8eaywqSUZo1DnIS+pkwsdipa7GgFl7A==}
- '@orpc/interop@1.12.2':
- resolution: {integrity: sha512-whHawJ8XZBzxngqOZKRzkI6HaFZcFSdbaK0//LmqOdSKXBeuveHF+kprCcpr8C6rH2N5i9nKMdnt0RetjPvxCg==}
-
'@orpc/interop@1.8.1':
resolution: {integrity: sha512-TA2AQREo3tKQheHPvVs5PY1PqDnNUFGO3c8eb6lrX53npOD6lMi6IPedzeEHaZCqb17bRZ9Skc9Fg1FZ30ckug==}
@@ -1487,22 +2044,11 @@ packages:
'@orpc/openapi@1.8.1':
resolution: {integrity: sha512-G1qMCjpVGxJYrU0vMdYOr2WxSiX9C6ZhvafgKN7vOGqC7O5s1D+Ph/cSUe1R9OPZZPouhw75MNjTtKYP5lOrcA==}
- '@orpc/otel@1.12.2':
- resolution: {integrity: sha512-51pE8OuncexO+ai5Ma1qZL3ldEL+msiHBN51SauuMezVXbCmVzhhHLlXD0LhvJWbE2X8kedLMmMYxI1fqiF0Mw==}
- peerDependencies:
- '@opentelemetry/api': '>=1.9.0'
- '@opentelemetry/instrumentation': '>=0.203.0'
-
- '@orpc/server@1.12.2':
- resolution: {integrity: sha512-lgT3VR+yXsCcgzbZ2d1fXtqaf1RbgUJHMDWQ4J22LBYH1P8pi0Nk+EYi9/w3YNFIr1WuUmVu4Pm6Dg6l92oiQA==}
+ '@orpc/otel@1.13.5':
+ resolution: {integrity: sha512-GPqaonr74EiqBbKLpNRVe6P8qv7CKM5mSLcfbkFcpvntSdl9OoPh+5Bq4iWhoaZF+7Ijs+/Ve9YHPr7LvRtJAA==}
peerDependencies:
- crossws: '>=0.3.4'
- ws: '>=8.18.1'
- peerDependenciesMeta:
- crossws:
- optional: true
- ws:
- optional: true
+ '@opentelemetry/api': '>=1.9.0'
+ '@opentelemetry/instrumentation': '>=0.203.0'
'@orpc/server@1.8.1':
resolution: {integrity: sha512-Aei7KJSMG9daNdubREeDYKCnmSRTnZT7V1Cx0NFLYDYqx8Bdil2J2InJghsio6AZdJUWLNq/zMm6QDu3NN2E6g==}
@@ -1515,8 +2061,8 @@ packages:
ws:
optional: true
- '@orpc/shared@1.12.2':
- resolution: {integrity: sha512-aITtDnmkofoG/GY6897AOPLnFMkLpQpM7ljzaqsG8QMbL6oovO427G/9Tr9Y3DDSyrsxA7FQ8+rwV03ZDG7gfQ==}
+ '@orpc/shared@1.13.5':
+ resolution: {integrity: sha512-yBFD9FqwazpbcOegEOZ0kAz7i9oNO110HX5AV5YAPGh+zxOY3RfZFXODQ5kBR1mr2nyo4ju+5ohYbJppZuWlcA==}
peerDependencies:
'@opentelemetry/api': '>=1.9.0'
peerDependenciesMeta:
@@ -1531,41 +2077,18 @@ packages:
'@opentelemetry/api':
optional: true
- '@orpc/standard-server-aws-lambda@1.12.2':
- resolution: {integrity: sha512-2gpM0ipl3YvE6/bJKTE6Oga4EROo2zuRiOzwY21F/3q38xyQ344Lk1smHzUv20lYZEIBDWP0FRR0OPhBQYLwNQ==}
-
'@orpc/standard-server-aws-lambda@1.8.1':
resolution: {integrity: sha512-5db2k1nhdySkYXMMXCxMuZIXDgo04rrZ6qqIEr1cvKnW5OHadcvxysfMa60WMA4Mzm3pypl2WJKGy36mdm6R8Q==}
- '@orpc/standard-server-fastify@1.12.2':
- resolution: {integrity: sha512-O1IsvytsK1j6cbuztreE5sJHL3OyvDMn1lSWKO16YoLBK47W8XRHCVBKDDV+1gk4f4Q74bSJOvNGQh1OUcNbkg==}
- peerDependencies:
- fastify: '>=5.6.1'
- peerDependenciesMeta:
- fastify:
- optional: true
-
- '@orpc/standard-server-fetch@1.12.2':
- resolution: {integrity: sha512-gjqZgD8uiW3dVaf+bo9Gj0GXTQ4B/vXkaHPmat+11AE+34UsOZJqZJzEuUi+P4qkmNOm6ZXAsb8roz6H3izPrg==}
-
'@orpc/standard-server-fetch@1.8.1':
resolution: {integrity: sha512-HZNTuLitB6X2nVPBfpbKvHRPNL7LV4J9+mQxr+Xi/hU2I6Fr2KWjNmAxcH1Fn6cOsJ09q9RFIaikUC88CQMnlA==}
- '@orpc/standard-server-node@1.12.2':
- resolution: {integrity: sha512-st9yjw3i+xFJu8YHeKcCBchMkRKyobBNqstR8yUelrLE/+rC0qKX+AwrmP6xq6gP6BQqFk+RSfIManaIruKuuQ==}
-
'@orpc/standard-server-node@1.8.1':
resolution: {integrity: sha512-+hKShhdAM2ljCbfvFjZQVhcpZjVXCgrz3ysTNKRsIR+E3aiYXffylL6RDOjLAjfvQyRy7wsPVxPjyqUHGXX4vg==}
- '@orpc/standard-server-peer@1.12.2':
- resolution: {integrity: sha512-O51FDFAHgK5uG5EhIG5PYWrkpnwBSVIKih2yYtNzLVYsU3y8EkE07UHRWixJFjUDJ7YeGy16ud3M8oNqt2bE4g==}
-
'@orpc/standard-server-peer@1.8.1':
resolution: {integrity: sha512-QDGfi/P0HKxFQCoDUOsIaIfO1KK10qoXDmd6ZfZF9DwFKrnqE6AEpMgz/TVDqLb6mFUzMtvdjrPga8uO9QdnbQ==}
- '@orpc/standard-server@1.12.2':
- resolution: {integrity: sha512-8ZNhL3CRJmoJ7uFjOZB4U7NVGdcbGIOKLRTP0i/yhi51QL5Lw+56f/hNqztrCPVZolhJWif86HjyQhuixizrVg==}
-
'@orpc/standard-server@1.8.1':
resolution: {integrity: sha512-SgVoxnmRAhrF0usZcz72F8FcneM4Rp+UYMhggwwnCEtjKtKXddg89J/to2bkpH/P5x8JMVI/uE2YXRG8CtU6Cg==}
@@ -1583,8 +2106,8 @@ packages:
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
- '@playwright/test@1.57.0':
- resolution: {integrity: sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==}
+ '@playwright/test@1.58.2':
+ resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==}
engines: {node: '>=18'}
hasBin: true
@@ -1789,46 +2312,52 @@ packages:
'@types/react':
optional: true
- '@rollup/rollup-linux-x64-gnu@4.53.3':
- resolution: {integrity: sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==}
+ '@rollup/rollup-linux-x64-gnu@4.57.1':
+ resolution: {integrity: sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==}
cpu: [x64]
os: [linux]
+ libc: [glibc]
+
+ '@rsbuild/core@1.6.0-beta.1':
+ resolution: {integrity: sha512-UjQnvXDW9m/hS4DP66ubGIMVjK2PzYx8tzgiinrO0kjNCr9i8KWuJSJGUWyczFMpSsXxp20LnuTxtx7kiGiYdA==}
+ engines: {node: '>=18.12.0'}
+ hasBin: true
- '@rsbuild/core@1.6.12':
- resolution: {integrity: sha512-gHwDfAMMgK2Zu9JgiekX949F262g6yYY+/ZLTIaMXHZmqYLAgsu0XOeVogpfQa045xcnwcQfpieiB9zgukJSgw==}
+ '@rsbuild/core@1.7.3':
+ resolution: {integrity: sha512-kI1oQvCXbQYxUvQPnDLdjSX4gFsbrFNpuUj6jXEJ7IcJ74Q+n4oeFj74/8tKerhxhe0L90m/ZQfzLeN5ORGA9w==}
engines: {node: '>=18.12.0'}
hasBin: true
- '@rsbuild/plugin-check-syntax@1.5.0':
- resolution: {integrity: sha512-/JutNle3wI67q/WOYcneyjUSAjZDJIycfpoJ04PWFh+qQ68Pwg4/Be1kU7GiVVklMwmEScoCd9S+gXWk+27YSg==}
+ '@rsbuild/plugin-check-syntax@1.6.1':
+ resolution: {integrity: sha512-26xtEYN0QjZYoyt0lWnvIztBWjEZJvcfw7MN4f5B4SpNggmnF7F7aNPrgkY3EccXVFx1VGQBhnCkBV//OoS07Q==}
peerDependencies:
- '@rsbuild/core': 1.x
+ '@rsbuild/core': ^1.0.0 || ^2.0.0-0
peerDependenciesMeta:
'@rsbuild/core':
optional: true
- '@rsdoctor/client@1.3.12':
- resolution: {integrity: sha512-7IsPn2JrsbuoCexKu4sEwPcrW1l/DeTnqG6oCp4QFvQ8fSMfNwG/ncigjIGyg4uPu5p5ZIBeY2G0p7c9120Z0w==}
+ '@rsdoctor/client@1.5.2':
+ resolution: {integrity: sha512-fufNlCiA4+MDj3ZW8ssEdRI9mwawdqSSYvqOOK01+NNIg3TuQNgEdnF/QVGnkxKLgVXJCtUdOKaZtUq1bwnJVQ==}
- '@rsdoctor/core@1.3.12':
- resolution: {integrity: sha512-9vsn7WWab+BQbdSrxIrLgv5jQ8l01UL9N5gCP3uLG6OUTRvKbDgn/iKtb1BobMkB5kdD2xV8iKNzb6oeTZpQtg==}
+ '@rsdoctor/core@1.5.2':
+ resolution: {integrity: sha512-pAtehxWiOhEef7+nxmeX7EDdABh9maAtmD+G/0MNC5zc/aKqzh2KD/CCrEO9SqSoVnfkTot1BQ54kLgBMFvclw==}
- '@rsdoctor/graph@1.3.12':
- resolution: {integrity: sha512-QqYm7SLybA8DW+4FVZQM5ooSfIFFBF6zTtCpyF3LO2pMFdN4OjX3Gmuaw01I9Qr5b/+gy5XiqsCFHXI0uVCbOQ==}
+ '@rsdoctor/graph@1.5.2':
+ resolution: {integrity: sha512-4Wt/Hg4Z3uSJBJQh1pm9cFeYI8lHhSFdWbR39dPOiIC/Q1/86TVdo63GBYMLYyXWRZ58S5kDL5ZjO9IyDLVgug==}
- '@rsdoctor/rspack-plugin@1.3.12':
- resolution: {integrity: sha512-LHulXsUudq+8lPt8JqsJ0/nPOdIVDqVlvEr1EhCNG9WUNFB2lQL+FzUluf1VpixjtzUM9OqeGY3M4rpSZcY/ww==}
+ '@rsdoctor/rspack-plugin@1.5.2':
+ resolution: {integrity: sha512-l097etdtvz4osG5rDJX0RBbIqLAZ+oSUXOJu11Y9aFg+4/lohrZZa4lj1R/7hxlqC0XuROir2zsbRaAuCGtxzw==}
peerDependencies:
'@rspack/core': '*'
peerDependenciesMeta:
'@rspack/core':
optional: true
- '@rsdoctor/sdk@1.3.12':
- resolution: {integrity: sha512-WOzF5CWLai2+8L3tmJZI5tdhodiHphgHbqOGD0N0RqrCFCeDx11oQkndXjXMns9kKd9Q0NhlzKjr76iE7QpjUQ==}
+ '@rsdoctor/sdk@1.5.2':
+ resolution: {integrity: sha512-VPJoSO1gPIlMDLumzGgphhIyNM8s7NPgbN/AAFwbU/uWzEwC2Gy2joLwoYkOw32UDdMD/MiuXrQoP+fnjAYjjQ==}
- '@rsdoctor/types@1.3.12':
- resolution: {integrity: sha512-/BvmO/cgqxBO1OJxGisYfNZOh2IdqORHUZRBbDIio8D88YQFt2Xo9mSBjHEEXXpPFe5Lw835tTOuCgAtQlw1EQ==}
+ '@rsdoctor/types@1.5.2':
+ resolution: {integrity: sha512-sOj7H/Dc9F3LPM/04dgJSpBhAb7DNlqcm9uipW6+ZCB/pr4dcwUDo8lHcjaEgQWeUfvHSUHjtzarp9EVpfkFsg==}
peerDependencies:
'@rspack/core': '*'
webpack: 5.x
@@ -1838,11 +2367,24 @@ packages:
webpack:
optional: true
- '@rsdoctor/utils@1.3.12':
- resolution: {integrity: sha512-vWvknXpowq2BI4jFZv0dfIg9GdLBlOqD4XW1EzMQ6ULYyPCU+b4C6o8OBn9Q5hEu/Ed0e2Ca4G3jTWYyFcsR+A==}
+ '@rsdoctor/utils@1.5.2':
+ resolution: {integrity: sha512-Zdvpp4GdJKgQYXLuILM124YU0peMDebr95k2s5zTTuB2LBkHENf8UdjJs7ijKyoaVYKqxXTBdghekXEFcdo6jQ==}
+
+ '@rslib/core@0.16.1':
+ resolution: {integrity: sha512-KhZwWO4kcP4PPdBH2aAgg/A6FYapbDh2rhsxo8dqOOPWCjDFU3QKAYPieKg+k4IrRQlye8AWOXo2rbyF4FC01g==}
+ engines: {node: '>=18.12.0'}
+ hasBin: true
+ peerDependencies:
+ '@microsoft/api-extractor': ^7
+ typescript: ^5
+ peerDependenciesMeta:
+ '@microsoft/api-extractor':
+ optional: true
+ typescript:
+ optional: true
- '@rslib/core@0.18.3':
- resolution: {integrity: sha512-Jdpssyig/OIJ9EzjXi957gKUCLZhlynMAunkJnaYcriFE245quLxZAdDsxXeeKjUp0Mx/J6fVXgUQyYmbKum6w==}
+ '@rslib/core@0.18.6':
+ resolution: {integrity: sha512-mMjfmtXRPHOCzO1A8KcAQckIHi/vA+qOHT/zK4M8Nnr5h1eAO6HIEN7AvJdgbIf5YXsVH6bXonrXdAwwP0gHow==}
engines: {node: '>=18.12.0'}
hasBin: true
peerDependencies:
@@ -1854,60 +2396,120 @@ packages:
typescript:
optional: true
- '@rspack/binding-darwin-arm64@1.6.6':
- resolution: {integrity: sha512-vGVDP0rlWa2w/gLba/sncVfkCah0HmhdmK5vGj/7sSX0iViwQneA2xjxDHyCNSQrvfq9GJmj4Kmdq/9tGh0KuA==}
+ '@rspack/binding-darwin-arm64@1.6.0-beta.1':
+ resolution: {integrity: sha512-RXQ97iVXgvQAb/cq265z/txdHOOJ6fQQRBfnn0IfMNk7gT4W2rvsLrOqQpwtMKxYV4N/mfWnycfAVa0OOf22Gg==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rspack/binding-darwin-arm64@1.7.6':
+ resolution: {integrity: sha512-NZ9AWtB1COLUX1tA9HQQvWpTy07NSFfKBU8A6ylWd5KH8AePZztpNgLLAVPTuNO4CZXYpwcoclf8jG/luJcQdQ==}
cpu: [arm64]
os: [darwin]
- '@rspack/binding-darwin-x64@1.6.6':
- resolution: {integrity: sha512-IcdEG2kOmbPPO70Zl7gDnowDjK7d7C1hWew2vU7dPltr2t1JalRIMnS051lhiur0ULkSxV3cW1zXqv0Oi8AnOg==}
+ '@rspack/binding-darwin-x64@1.6.0-beta.1':
+ resolution: {integrity: sha512-Ulb7Jyyvuf28BwPXZKSbglaSK/19b32ItWT+pgswhbFsnfhzAQQd7Jo7TUEvHNHAdVDiES8VFlrnOhOSnwEOLg==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rspack/binding-darwin-x64@1.7.6':
+ resolution: {integrity: sha512-J2g6xk8ZS7uc024dNTGTHxoFzFovAZIRixUG7PiciLKTMP78svbSSWrmW6N8oAsAkzYfJWwQpVgWfFNRHvYxSw==}
cpu: [x64]
os: [darwin]
- '@rspack/binding-linux-arm64-gnu@1.6.6':
- resolution: {integrity: sha512-rIguCCtlTcwoFlwheDiUgdImk27spuCRn43zGJogARpM/ZYRFKIuSwFDGUtJT2g0TSLUAHUhWAUqC36NwvrbMQ==}
+ '@rspack/binding-linux-arm64-gnu@1.6.0-beta.1':
+ resolution: {integrity: sha512-UyUoh5RXHTWCktqPVnqoc5rwlWyLkWqGu6ga+iyJHDxdxlrHFfwJnTSnCd4y8cRadf7CrmjHElxE61GU3WCYhw==}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
- '@rspack/binding-linux-arm64-musl@1.6.6':
- resolution: {integrity: sha512-x6X6Gr0fUw6qrJGxZt3Rb6oIX+jd9pdcyp0VbtofcLaqGVQbzustYsYnuLATPOys0q4J/4kWnmEhkjLJHwkhpQ==}
+ '@rspack/binding-linux-arm64-gnu@1.7.6':
+ resolution: {integrity: sha512-eQfcsaxhFrv5FmtaA7+O1F9/2yFDNIoPZzV/ZvqvFz5bBXVc4FAm/1fVpBg8Po/kX1h0chBc7Xkpry3cabFW8w==}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
- '@rspack/binding-linux-x64-gnu@1.6.6':
- resolution: {integrity: sha512-gSlVdASszWHosQKn+nzYOInBijdQboUnmNMGgW9/PijVg3433IvQjzviUuJFno8CMGgrACV9yw+ZFDuK0J57VA==}
+ '@rspack/binding-linux-arm64-musl@1.6.0-beta.1':
+ resolution: {integrity: sha512-JAXVKHQieN4Ruvs7MstvsPUtRBSAROqJ0abCh4rXdV+FzncKp/ZkdfjQploDhBWtWfU8rPvIjaxeZcPfHMI5/A==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@rspack/binding-linux-arm64-musl@1.7.6':
+ resolution: {integrity: sha512-DfQXKiyPIl7i1yECHy4eAkSmlUzzsSAbOjgMuKn7pudsWf483jg0UUYutNgXSlBjc/QSUp7906Cg8oty9OfwPA==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@rspack/binding-linux-x64-gnu@1.6.0-beta.1':
+ resolution: {integrity: sha512-LqAos71CJS5/V4knX9T7T68oGz0XPRZ2IJmI3jEByRlNcyZdxYeQ7Dw09JO9Y5Xj0T+0cudOeL2MxHcD3gTF/w==}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rspack/binding-linux-x64-gnu@1.7.6':
+ resolution: {integrity: sha512-NdA+2X3lk2GGrMMnTGyYTzM3pn+zNjaqXqlgKmFBXvjfZqzSsKq3pdD1KHZCd5QHN+Fwvoszj0JFsquEVhE1og==}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rspack/binding-linux-x64-musl@1.6.0-beta.1':
+ resolution: {integrity: sha512-E4dRMzIHYaoYkgmDTFLrgnGtdspbAuVbLfaPF9AWW5YkQn52obGAgbbNb1wi1JJ5f29nTBoLauYCucEO5IGFvA==}
cpu: [x64]
os: [linux]
+ libc: [musl]
- '@rspack/binding-linux-x64-musl@1.6.6':
- resolution: {integrity: sha512-TZaqVkh7memsTK/hxkOBrbpdzbmBUMea1YnYt++7QjMgco1kWFvAQ+YhAWtIaOaEg8s6C07Lt0Zp8izM2Dja0g==}
+ '@rspack/binding-linux-x64-musl@1.7.6':
+ resolution: {integrity: sha512-rEy6MHKob02t/77YNgr6dREyJ0e0tv1X6Xsg8Z5E7rPXead06zefUbfazj4RELYySWnM38ovZyJAkPx/gOn3VA==}
cpu: [x64]
os: [linux]
+ libc: [musl]
- '@rspack/binding-wasm32-wasi@1.6.6':
- resolution: {integrity: sha512-W4mWdlLnYrbUaktyHOGNfATblxMTbgF7CBfDw8PhbDtjd2l8e/TnaHgIDkwITHXAOMEF/QEKfo9FtusbcQJNKw==}
+ '@rspack/binding-wasm32-wasi@1.6.0-beta.1':
+ resolution: {integrity: sha512-PaKEjXOkYprSFlgdgVm/P3pv2E8nAQx9WSGgPmMVIAtxo3Cyz0wwFf0f1Bp9wCw0KkIWgi+9lz8oXNkgKZilug==}
cpu: [wasm32]
- '@rspack/binding-win32-arm64-msvc@1.6.6':
- resolution: {integrity: sha512-cw5OgxqoDwjoZlk0L3vGEwcjPZsOVFYLwr2ssiC05rsTbhBwxj8coLpAJdvUvbf6C2TTmCB7iPe2sPq1KWD37g==}
+ '@rspack/binding-wasm32-wasi@1.7.6':
+ resolution: {integrity: sha512-YupOrz0daSG+YBbCIgpDgzfMM38YpChv+afZpaxx5Ml7xPeAZIIdgWmLHnQ2rts73N2M1NspAiBwV00Xx0N4Vg==}
+ cpu: [wasm32]
+
+ '@rspack/binding-win32-arm64-msvc@1.6.0-beta.1':
+ resolution: {integrity: sha512-HWz9Qxrjf3TKLCwiFPJaqw+STvEsBvFYZvBXZ8umIZXqtdfgQP5d91V8JRG4Gg1J6xnGC/KhZexxBuR/y64aBA==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rspack/binding-win32-arm64-msvc@1.7.6':
+ resolution: {integrity: sha512-INj7aVXjBvlZ84kEhSK4kJ484ub0i+BzgnjDWOWM1K+eFYDZjLdAsQSS3fGGXwVc3qKbPIssFfnftATDMTEJHQ==}
cpu: [arm64]
os: [win32]
- '@rspack/binding-win32-ia32-msvc@1.6.6':
- resolution: {integrity: sha512-M4ruR+VZ59iy+mPjy6FQPT27cOgeytf3wFBrt7e0suKeNLYGxrNyI9YhgpCTY++SMJsAMgRLGDHoI3ZgWulw1Q==}
+ '@rspack/binding-win32-ia32-msvc@1.6.0-beta.1':
+ resolution: {integrity: sha512-alAZHRuyPzCH3rJpEC9EBE60EZPnQjzltZ6HN8lsCidACMFTzaLBvuzZyYQah+Zm58O22ok2Eon4BpP1Coizgg==}
+ cpu: [ia32]
+ os: [win32]
+
+ '@rspack/binding-win32-ia32-msvc@1.7.6':
+ resolution: {integrity: sha512-lXGvC+z67UMcw58In12h8zCa9IyYRmuptUBMItQJzu+M278aMuD1nETyGLL7e4+OZ2lvrnnBIcjXN1hfw2yRzw==}
cpu: [ia32]
os: [win32]
- '@rspack/binding-win32-x64-msvc@1.6.6':
- resolution: {integrity: sha512-q5QTvdhPUh+CA93cQG5zWKRIHMIWPzw+ftFDEwBw52zYdvNAoLniqD8o5Mi8CT0pndhulXgR5aw0Sjd3eMah+A==}
+ '@rspack/binding-win32-x64-msvc@1.6.0-beta.1':
+ resolution: {integrity: sha512-/WBzhed0Cu0o9XQ9caGgWwzyNnnPKlENlExa2aGbRCbB14/+CwfhCyETyKlc/ID+dtlV/eHKTC9cckUNI8NpTQ==}
cpu: [x64]
os: [win32]
- '@rspack/binding@1.6.6':
- resolution: {integrity: sha512-noiV+qhyBTVpvG2M4bnOwKk2Ynl6G47Wf7wpCjPCFr87qr3txNwTTnhkEJEU59yj+VvIhbRD2rf5+9TLoT0Wxg==}
+ '@rspack/binding-win32-x64-msvc@1.7.6':
+ resolution: {integrity: sha512-zeUxEc0ZaPpmaYlCeWcjSJUPuRRySiSHN23oJ2Xyw0jsQ01Qm4OScPdr0RhEOFuK/UE+ANyRtDo4zJsY52Hadw==}
+ cpu: [x64]
+ os: [win32]
+
+ '@rspack/binding@1.6.0-beta.1':
+ resolution: {integrity: sha512-r3L60ekkDLM5qoRjCMrqsgwU9SQ5e8oA/Omltu/FEEUspIVHawPvAqNZvAXnGB+FoNxM8YgdRRh12PAwXJww0A==}
+
+ '@rspack/binding@1.7.6':
+ resolution: {integrity: sha512-/NrEcfo8Gx22hLGysanrV6gHMuqZSxToSci/3M4kzEQtF5cPjfOv5pqeLK/+B6cr56ul/OmE96cCdWcXeVnFjQ==}
- '@rspack/core@1.6.6':
- resolution: {integrity: sha512-2mR+2YBydlgZ7Q0Rpd6bCC3MBnV9TS0x857K0zIhbDj4BQOqaWVy1n7fx/B3MrS8TR0QCuzKfyDAjNz+XTyJVQ==}
+ '@rspack/core@1.6.0-beta.1':
+ resolution: {integrity: sha512-2ff8XWonPPHyQ6mEWogMspg+Sul3lXZUfNQVrbYSjfNpi8CeDV0/ZtRbHHbAXiy6pz5fvBFL6X+i/ATckjTYBw==}
engines: {node: '>=18.12.0'}
peerDependencies:
'@swc/helpers': '>=0.5.1'
@@ -1915,6 +2517,19 @@ packages:
'@swc/helpers':
optional: true
+ '@rspack/core@1.7.6':
+ resolution: {integrity: sha512-Iax6UhrfZqJajA778c1d5DBFbSIqPOSrI34kpNIiNpWd8Jq7mFIa+Z60SQb5ZQDZuUxcCZikjz5BxinFjTkg7Q==}
+ engines: {node: '>=18.12.0'}
+ peerDependencies:
+ '@swc/helpers': '>=0.5.1'
+ peerDependenciesMeta:
+ '@swc/helpers':
+ optional: true
+
+ '@rspack/lite-tapable@1.0.1':
+ resolution: {integrity: sha512-VynGOEsVw2s8TAlLf/uESfrgfrq2+rcXB1muPJYBWbsm1Oa6r5qVQhjA5ggM6z/coYPrsVMgovl3Ff7Q7OCp1w==}
+ engines: {node: '>=16.0.0'}
+
'@rspack/lite-tapable@1.1.0':
resolution: {integrity: sha512-E2B0JhYFmVAwdDiG14+DW0Di4Ze4Jg10Pc4/lILUrd5DRCaklduz2OvJ5HYQ6G+hd+WTzqQb3QnDNfK4yvAFYw==}
@@ -1924,17 +2539,17 @@ packages:
'@sqltools/formatter@1.2.5':
resolution: {integrity: sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==}
- '@standard-schema/spec@1.0.0':
- resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
+ '@standard-schema/spec@1.1.0':
+ resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
'@swc/helpers@0.5.15':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
- '@swc/helpers@0.5.17':
- resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==}
+ '@swc/helpers@0.5.18':
+ resolution: {integrity: sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==}
- '@trpc/server@11.7.2':
- resolution: {integrity: sha512-AgB26PXY69sckherIhCacKLY49rxE2XP5h38vr/KMZTbLCL1p8IuIoKPjALTcugC2kbyQ7Lbqo2JDVfRSmPmfQ==}
+ '@trpc/server@11.10.0':
+ resolution: {integrity: sha512-zZjTrR6He61e5TiT7e/bQqab/jRcXBZM8Fg78Yoo8uh5pz60dzzbYuONNUCOkafv5ppXVMms4NHYfNZgzw50vg==}
peerDependencies:
typescript: '>=5.7.2'
@@ -1953,16 +2568,22 @@ packages:
'@types/estree@1.0.5':
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
- '@types/node@24.10.1':
- resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==}
+ '@types/figlet@1.7.0':
+ resolution: {integrity: sha512-KwrT7p/8Eo3Op/HBSIwGXOsTZKYiM9NpWRBJ5sVjWP/SmlS+oxxRvJht/FNAtliJvja44N3ul1yATgohnVBV0Q==}
+
+ '@types/node@24.10.13':
+ resolution: {integrity: sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==}
+
+ '@types/node@25.2.3':
+ resolution: {integrity: sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==}
'@types/react-dom@19.2.3':
resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
peerDependencies:
'@types/react': ^19.2.0
- '@types/react@19.2.7':
- resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==}
+ '@types/react@19.2.14':
+ resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==}
'@types/tapable@2.2.7':
resolution: {integrity: sha512-D6QzACV9vNX3r8HQQNTOnpG+Bv1rko+yEA82wKs3O9CQ5+XW7HI7TED17/UE7+5dIxyxZIWTxKbsBeF6uKFCwA==}
@@ -2004,8 +2625,8 @@ packages:
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
engines: {node: '>=12'}
- ansis@4.2.0:
- resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==}
+ ansis@3.17.0:
+ resolution: {integrity: sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==}
engines: {node: '>=14'}
app-root-path@3.1.0:
@@ -2029,13 +2650,13 @@ packages:
resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==}
engines: {node: ^4.5.0 || >= 5.9}
- baseline-browser-mapping@2.9.2:
- resolution: {integrity: sha512-PxSsosKQjI38iXkmb3d0Y32efqyA0uW4s41u4IVBsLlWLhCiYNpH/AfNOVWRqCQBlD8TFJTz6OUWNd4DFJCnmw==}
+ baseline-browser-mapping@2.9.19:
+ resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==}
hasBin: true
- better-sqlite3@12.5.0:
- resolution: {integrity: sha512-WwCZ/5Diz7rsF29o27o0Gcc1Du+l7Zsv7SYtVPG0X3G/uUI1LqdxrQI7c9Hs2FWpqXXERjW9hp6g3/tH7DlVKg==}
- engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x}
+ better-sqlite3@12.4.1:
+ resolution: {integrity: sha512-3yVdyZhklTiNrtg+4WqHpJpFDd+WHTg2oM7UcR80GqL05AOV0xEJzc6qNvFYoEtE+hRp1n9MpN6/+4yhlGkDXQ==}
+ engines: {node: 20.x || 22.x || 23.x || 24.x}
bindings@1.5.0:
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
@@ -2049,8 +2670,9 @@ packages:
browserslist-load-config@1.0.1:
resolution: {integrity: sha512-orLR5HAoQugQNVUXUwNd+GAAwl3H64KLIwoMFBNW0AbMSqX2Lxs4ZV2/5UoNrVQlQqF9ygychiu7Svr/99bLtg==}
- browserslist-to-es-version@1.2.0:
- resolution: {integrity: sha512-wZpJM7QUP33yPzWDzMLgTFZzb3WC6f7G13gnJ5p8PlFz3Xm9MUwArRh9jgE0y4/Sqo6CKtsNxyGpVf5zLWgAhg==}
+ browserslist-to-es-version@1.4.1:
+ resolution: {integrity: sha512-1bYCrck5Qh5HUy7P+iDuK39v757/ry5PnQo20vf4sHGeUrYKL2N2OF05U9ARSGt06TpFDQiTv9MT+eitYgWWxA==}
+ hasBin: true
browserslist@4.28.1:
resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==}
@@ -2066,6 +2688,9 @@ packages:
buffer@6.0.3:
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
+ c15t@1.7.1:
+ resolution: {integrity: sha512-MXQt0zsqTRbUs+9dNc9PivauJWc9232akbotS3NmUXnkMLBxYsCduSvqe560HDVVj0uhIQmYx0Yp0JbvHp/5yw==}
+
c15t@1.8.1:
resolution: {integrity: sha512-D4We2FxVN7tGf/LQsMCWQj+GrSVKX6HSC9Leyw/Ble74E0ocmVKI61+YcP6rdEpmIv6e9T9lO7NECtX0RTEEWA==}
@@ -2081,8 +2706,8 @@ packages:
resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
engines: {node: '>= 0.4'}
- caniuse-lite@1.0.30001759:
- resolution: {integrity: sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==}
+ caniuse-lite@1.0.30001770:
+ resolution: {integrity: sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==}
chalk@5.6.2:
resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==}
@@ -2091,12 +2716,15 @@ packages:
chownr@1.1.4:
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
- citty@0.1.6:
- resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==}
+ citty@0.2.1:
+ resolution: {integrity: sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg==}
cjs-module-lexer@1.4.3:
resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==}
+ cjs-module-lexer@2.2.0:
+ resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==}
+
cli-table3@0.6.5:
resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==}
engines: {node: 10.* || >= 12.*}
@@ -2119,17 +2747,10 @@ packages:
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
- commander@14.0.2:
- resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==}
+ commander@14.0.3:
+ resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==}
engines: {node: '>=20'}
- confbox@0.2.2:
- resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==}
-
- consola@3.4.2:
- resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==}
- engines: {node: ^14.18.0 || >=16.10.0}
-
cookie@0.7.2:
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
engines: {node: '>= 0.6'}
@@ -2142,11 +2763,14 @@ packages:
resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==}
engines: {node: '>=18'}
+ core-js@3.46.0:
+ resolution: {integrity: sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA==}
+
core-js@3.47.0:
resolution: {integrity: sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==}
- cors@2.8.5:
- resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==}
+ cors@2.8.6:
+ resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
engines: {node: '>= 0.10'}
cross-spawn@7.0.6:
@@ -2185,8 +2809,8 @@ packages:
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
engines: {node: '>=10'}
- dedent@1.7.0:
- resolution: {integrity: sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==}
+ dedent@1.7.1:
+ resolution: {integrity: sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==}
peerDependencies:
babel-plugin-macros: ^3.1.0
peerDependenciesMeta:
@@ -2237,12 +2861,12 @@ packages:
resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==}
engines: {node: '>=12'}
- dotenv@17.2.3:
- resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==}
+ dotenv@17.3.1:
+ resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==}
engines: {node: '>=12'}
- drizzle-kit@0.31.8:
- resolution: {integrity: sha512-O9EC/miwdnRDY10qRxM8P3Pg8hXe3LyU4ZipReKOgTwn4OqANmftj8XJz1UPUAS6NMHf0E2htjsbQujUTkncCg==}
+ drizzle-kit@0.31.9:
+ resolution: {integrity: sha512-GViD3IgsXn7trFyBUUHyTFBpH/FsHTxYJ66qdbVggxef4UBPHRYxQaRzYLTuekYnk9i5FIEL9pbBIwMqX/Uwrg==}
hasBin: true
drizzle-orm@0.44.7:
@@ -2344,8 +2968,8 @@ packages:
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
- electron-to-chromium@1.5.266:
- resolution: {integrity: sha512-kgWEglXvkEfMH7rxP5OSZZwnaDWT7J9EoZCujhnpLbfi0bbNtRkgdX2E3gt0Uer11c61qCYktB3hwkAS325sJg==}
+ electron-to-chromium@1.5.286:
+ resolution: {integrity: sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==}
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
@@ -2360,8 +2984,8 @@ packages:
resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==}
engines: {node: '>=10.0.0'}
- engine.io@6.6.4:
- resolution: {integrity: sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==}
+ engine.io@6.6.5:
+ resolution: {integrity: sha512-2RZdgEbXmp5+dVbRm0P7HQUImZpICccJy7rN7Tv+SFa55pH+lxnuw6/K1ZxxBfHoYpSkHLAO92oa8O4SwFXA2A==}
engines: {node: '>=10.2.0'}
enhanced-resolve@5.12.0:
@@ -2376,8 +3000,8 @@ packages:
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
engines: {node: '>=0.12'}
- envinfo@7.19.0:
- resolution: {integrity: sha512-DoSM9VyG6O3vqBf+p3Gjgr/Q52HYBBtO3v+4koAxt1MnWr+zEnxE+nke/yXS4lt2P4SYCHQ4V3f1i88LQVOpAw==}
+ envinfo@7.21.0:
+ resolution: {integrity: sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==}
engines: {node: '>=4'}
hasBin: true
@@ -2393,8 +3017,8 @@ packages:
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
engines: {node: '>= 0.4'}
- es-toolkit@1.42.0:
- resolution: {integrity: sha512-SLHIyY7VfDJBM8clz4+T2oquwTQxEzu263AyhVK4jREOAwJ+8eebaa4wM3nlvnAqhDrMm2EsA6hWHaQsMPQ1nA==}
+ es-toolkit@1.44.0:
+ resolution: {integrity: sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==}
esbuild-register@3.6.0:
resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==}
@@ -2423,13 +3047,15 @@ packages:
resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
engines: {node: '>=6'}
- exsolve@1.0.8:
- resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==}
-
fetch-blob@3.2.0:
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
engines: {node: ^12.20 || >= 14.13}
+ figlet@1.10.0:
+ resolution: {integrity: sha512-aktIwEZZ6Gp9AWdMXW4YCi0J2Ahuxo67fNJRUIWD81w8pQ0t9TS8FFpbl27ChlTLF06VkwjDesZSzEVzN75rzA==}
+ engines: {node: '>= 17.0.0'}
+ hasBin: true
+
file-uri-to-path@1.0.0:
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
@@ -2452,8 +3078,8 @@ packages:
fs-constants@1.0.0:
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
- fs-extra@11.3.2:
- resolution: {integrity: sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==}
+ fs-extra@11.3.3:
+ resolution: {integrity: sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==}
engines: {node: '>=14.14'}
fsevents@2.3.2:
@@ -2500,20 +3126,17 @@ packages:
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
engines: {node: '>= 0.4'}
- get-tsconfig@4.13.0:
- resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==}
+ get-tsconfig@4.13.6:
+ resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==}
github-from-package@0.0.0:
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
glob@10.5.0:
resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==}
+ deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
hasBin: true
- glob@13.0.0:
- resolution: {integrity: sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==}
- engines: {node: 20 || >=22}
-
gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'}
@@ -2545,8 +3168,8 @@ packages:
import-in-the-middle@1.15.0:
resolution: {integrity: sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==}
- import-in-the-middle@2.0.0:
- resolution: {integrity: sha512-yNZhyQYqXpkT0AKq3F3KLasUSK4fHvebNH5hOsKQw2dhGSALvQ4U0BqUc5suziKvydO5u5hgN2hy1RJaho8U5A==}
+ import-in-the-middle@2.0.6:
+ resolution: {integrity: sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw==}
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
@@ -2612,8 +3235,8 @@ packages:
resolution: {integrity: sha512-FIyV/64EkKhJmjgC0g2hygpBv5RNWVPyNCqSAD7eTCv6eFWNIi4PN1UvdSJGicN/o35bnevgis4Y0UDC0qi8jQ==}
engines: {node: '>=14.0.0'}
- kysely@0.28.8:
- resolution: {integrity: sha512-QUOgl5ZrS9IRuhq5FvOKFSsD/3+IA6MLE81/bOOTRA/YQpKDza2sFdN5g6JCB9BOpqMJDGefLCQ9F12hRS13TA==}
+ kysely@0.28.11:
+ resolution: {integrity: sha512-zpGIFg0HuoC893rIjYX1BETkVWdDnzTzF5e0kWXJFg5lE0k1/LfNWBejrcnOFu8Q2Rfq/hTDTU7XLUM8QOrpzg==}
engines: {node: '>=20.0.0'}
libsql@0.5.22:
@@ -2638,10 +3261,6 @@ packages:
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
- lru-cache@11.2.4:
- resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==}
- engines: {node: 20 || >=22}
-
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
@@ -2658,10 +3277,6 @@ packages:
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
engines: {node: '>=10'}
- minimatch@10.1.1:
- resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==}
- engines: {node: 20 || >=22}
-
minimatch@9.0.5:
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
engines: {node: '>=16 || 14 >=14.17'}
@@ -2698,8 +3313,8 @@ packages:
resolution: {integrity: sha512-kOCT/1MCPAxY5iUV3wytNFUMUolzuwd/VF/1KCx7kf6CutrOsTie+84zTGTpgQycjvfLdBBdvBvFLqFD2c0wkQ==}
engines: {node: '>=18'}
- next@16.0.7:
- resolution: {integrity: sha512-3mBRJyPxT4LOxAJI6IsXeFtKfiJUbjCLgvXO02fV8Wy/lIhPvP94Fe7dGhUgHXcQy4sSuYwQNcOLhIfOm0rL0A==}
+ next@16.1.6:
+ resolution: {integrity: sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==}
engines: {node: '>=20.9.0'}
hasBin: true
peerDependencies:
@@ -2719,8 +3334,8 @@ packages:
sass:
optional: true
- node-abi@3.85.0:
- resolution: {integrity: sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==}
+ node-abi@3.87.0:
+ resolution: {integrity: sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==}
engines: {node: '>=10'}
node-domexception@1.0.0:
@@ -2735,9 +3350,9 @@ packages:
node-releases@2.0.27:
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
- nypm@0.6.2:
- resolution: {integrity: sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==}
- engines: {node: ^14.16.0 || >=16.10.0}
+ nypm@0.6.5:
+ resolution: {integrity: sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==}
+ engines: {node: '>=18'}
hasBin: true
object-assign@4.1.1:
@@ -2750,10 +3365,6 @@ packages:
openapi-types@12.1.3:
resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==}
- p-limit@7.2.0:
- resolution: {integrity: sha512-ATHLtwoTNDloHRFFxFJdHnG6n2WUeFjaR8XQMFdKIv0xkXjrER8/iG9iu265jOM95zXHAfv9oTkqhrfbIzosrQ==}
- engines: {node: '>=20'}
-
package-json-from-dist@1.0.1:
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
@@ -2778,32 +3389,29 @@ packages:
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
engines: {node: '>=16 || 14 >=14.18'}
- path-scurry@2.0.1:
- resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==}
- engines: {node: 20 || >=22}
-
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
+ perfume.js@9.4.0:
+ resolution: {integrity: sha512-YYxGBYm2OcDx68GhzX/N3h4RrtViAz7Whgk7dA6j1bC9NxBGIG8c+rs+K3ql/dW4KReszN8ptAC1ghUGdWNsMQ==}
+ engines: {node: '>=10.0.0'}
+
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
- pkg-types@2.3.0:
- resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==}
-
- playwright-core@1.57.0:
- resolution: {integrity: sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==}
+ playwright-core@1.58.2:
+ resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==}
engines: {node: '>=18'}
hasBin: true
- playwright-performance-metrics@1.2.4:
- resolution: {integrity: sha512-opcX5KR3TZLn6oHkRHvoLBdp0U20Yact/gdKTCGN3uwnp3SmB5Ch8knlAJOujQpLN/WZ/JAI/hlUaesewPq7fg==}
+ playwright-performance-metrics@1.2.5:
+ resolution: {integrity: sha512-64tL3xm3TCn3J1yYhrW6pCYbx9uaJYWy4l3aD7hLK2BPLub7U1fYL0yVpI5ewLq4qMwsal8OkomShuMflbGUBA==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@playwright/test': '>=1.40.0 <2.0.0'
- playwright@1.57.0:
- resolution: {integrity: sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==}
+ playwright@1.58.2:
+ resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==}
engines: {node: '>=18'}
hasBin: true
@@ -2845,16 +3453,16 @@ packages:
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
hasBin: true
- react-dom@19.2.1:
- resolution: {integrity: sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==}
+ react-dom@19.2.4:
+ resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==}
peerDependencies:
- react: ^19.2.1
+ react: ^19.2.4
react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
- react@19.2.1:
- resolution: {integrity: sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==}
+ react@19.2.4:
+ resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==}
engines: {node: '>=0.10.0'}
readable-stream@3.6.2:
@@ -2884,11 +3492,27 @@ packages:
engines: {node: '>= 0.4'}
hasBin: true
- rou3@0.7.10:
- resolution: {integrity: sha512-aoFj6f7MJZ5muJ+Of79nrhs9N3oLGqi2VEMe94Zbkjb6Wupha46EuoYgpWSOZlXww3bbd8ojgXTAA2mzimX5Ww==}
+ rou3@0.7.12:
+ resolution: {integrity: sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==}
+
+ rsbuild-plugin-dts@0.16.1:
+ resolution: {integrity: sha512-2/5ihqhc6q42gor9KsS+Kyeqv3hY3wt65tsL7gIRLjjBrlrVeo2MIyTjXcFpJ6hgLslJzm2KITpIDr9nxs30CA==}
+ engines: {node: '>=18.12.0'}
+ peerDependencies:
+ '@microsoft/api-extractor': ^7
+ '@rsbuild/core': 1.x
+ '@typescript/native-preview': 7.x
+ typescript: ^5
+ peerDependenciesMeta:
+ '@microsoft/api-extractor':
+ optional: true
+ '@typescript/native-preview':
+ optional: true
+ typescript:
+ optional: true
- rsbuild-plugin-dts@0.18.3:
- resolution: {integrity: sha512-+V0D37pK0Q+5DIVGDlq+ky5H/Sb2VnTzsbtV3l0Hw9tVcXQOjEQMeWAmBgC8jBgNjnCVEWrwCqLdAvSTzIeOOw==}
+ rsbuild-plugin-dts@0.18.6:
+ resolution: {integrity: sha512-E0KxiXxY5T3D+DjMMQS3/DMlNwjzNp1xEYyH68gwI7Ptm3a1ChawvlZ1DqVW5m8NWkdbCbcAyazwOnDD3yVOLQ==}
engines: {node: '>=18.12.0'}
peerDependencies:
'@microsoft/api-extractor': ^7
@@ -2915,8 +3539,8 @@ packages:
scheduler@0.27.0:
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
- semver@7.7.3:
- resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
+ semver@7.7.4:
+ resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==}
engines: {node: '>=10'}
hasBin: true
@@ -2954,11 +3578,11 @@ packages:
sisteransi@1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
- socket.io-adapter@2.5.5:
- resolution: {integrity: sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==}
+ socket.io-adapter@2.5.6:
+ resolution: {integrity: sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==}
- socket.io-parser@4.2.4:
- resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==}
+ socket.io-parser@4.2.5:
+ resolution: {integrity: sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==}
engines: {node: '>=10.0.0'}
socket.io@4.8.1:
@@ -3055,8 +3679,8 @@ packages:
resolution: {integrity: sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==}
engines: {node: '>= 0.4'}
- trpc-cli@0.12.1:
- resolution: {integrity: sha512-/D/mIQf3tUrS7ZKJZ1gmSPJn2psAABJfkC5Eevm55SZ4s6KwANOUNlwhAGXN9HT4VSJVfoF2jettevE9vHPQlg==}
+ trpc-cli@0.12.2:
+ resolution: {integrity: sha512-kGNCiyOimGlfcZFImbWzFF2Nn3TMnenwUdyuckiN5SEaceJbIac7+Iau3WsVHjQpoNgugFruZMDOKf8GNQNtJw==}
engines: {node: '>=18'}
hasBin: true
peerDependencies:
@@ -3086,38 +3710,38 @@ packages:
tunnel-agent@0.6.0:
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
- turbo-darwin-64@2.6.3:
- resolution: {integrity: sha512-BlJJDc1CQ7SK5Y5qnl7AzpkvKSnpkfPmnA+HeU/sgny3oHZckPV2776ebO2M33CYDSor7+8HQwaodY++IINhYg==}
+ turbo-darwin-64@2.8.9:
+ resolution: {integrity: sha512-KnCw1ZI9KTnEAhdI9avZrnZ/z4wsM++flMA1w8s8PKOqi5daGpFV36qoPafg4S8TmYMe52JPWEoFr0L+lQ5JIw==}
cpu: [x64]
os: [darwin]
- turbo-darwin-arm64@2.6.3:
- resolution: {integrity: sha512-MwVt7rBKiOK7zdYerenfCRTypefw4kZCue35IJga9CH1+S50+KTiCkT6LBqo0hHeoH2iKuI0ldTF2a0aB72z3w==}
+ turbo-darwin-arm64@2.8.9:
+ resolution: {integrity: sha512-CbD5Y2NKJKBXTOZ7z7Cc7vGlFPZkYjApA7ri9lH4iFwKV1X7MoZswh9gyRLetXYWImVX1BqIvP8KftulJg/wIA==}
cpu: [arm64]
os: [darwin]
- turbo-linux-64@2.6.3:
- resolution: {integrity: sha512-cqpcw+dXxbnPtNnzeeSyWprjmuFVpHJqKcs7Jym5oXlu/ZcovEASUIUZVN3OGEM6Y/OTyyw0z09tOHNt5yBAVg==}
+ turbo-linux-64@2.8.9:
+ resolution: {integrity: sha512-OXC9HdCtsHvyH+5KUoH8ds+p5WU13vdif0OPbsFzZca4cUXMwKA3HWwUuCgQetk0iAE4cscXpi/t8A263n3VTg==}
cpu: [x64]
os: [linux]
- turbo-linux-arm64@2.6.3:
- resolution: {integrity: sha512-MterpZQmjXyr4uM7zOgFSFL3oRdNKeflY7nsjxJb2TklsYqiu3Z9pQ4zRVFFH8n0mLGna7MbQMZuKoWqqHb45w==}
+ turbo-linux-arm64@2.8.9:
+ resolution: {integrity: sha512-yI5n8jNXiFA6+CxnXG0gO7h5ZF1+19K8uO3/kXPQmyl37AdiA7ehKJQOvf9OPAnmkGDHcF2HSCPltabERNRmug==}
cpu: [arm64]
os: [linux]
- turbo-windows-64@2.6.3:
- resolution: {integrity: sha512-biDU70v9dLwnBdLf+daoDlNJVvqOOP8YEjqNipBHzgclbQlXbsi6Gqqelp5er81Qo3BiRgmTNx79oaZQTPb07Q==}
+ turbo-windows-64@2.8.9:
+ resolution: {integrity: sha512-/OztzeGftJAg258M/9vK2ZCkUKUzqrWXJIikiD2pm8TlqHcIYUmepDbyZSDfOiUjMy6NzrLFahpNLnY7b5vNgg==}
cpu: [x64]
os: [win32]
- turbo-windows-arm64@2.6.3:
- resolution: {integrity: sha512-dDHVKpSeukah3VsI/xMEKeTnV9V9cjlpFSUs4bmsUiLu3Yv2ENlgVEZv65wxbeE0bh0jjpmElDT+P1KaCxArQQ==}
+ turbo-windows-arm64@2.8.9:
+ resolution: {integrity: sha512-xZ2VTwVTjIqpFZKN4UBxDHCPM3oJ2J5cpRzCBSmRpJ/Pn33wpiYjs+9FB2E03svKaD04/lSSLlEUej0UYsugfg==}
cpu: [arm64]
os: [win32]
- turbo@2.6.3:
- resolution: {integrity: sha512-bf6YKUv11l5Xfcmg76PyWoy/e2vbkkxFNBGJSnfdSXQC33ZiUfutYh6IXidc5MhsnrFkWfdNNLyaRk+kHMLlwA==}
+ turbo@2.8.9:
+ resolution: {integrity: sha512-G+Mq8VVQAlpz/0HTsxiNNk/xywaHGl+dk1oiBREgOEVCCDjXInDlONWUn5srRnC9s5tdHTFD1bx1N19eR4hI+g==}
hasBin: true
type-detect@4.1.0:
@@ -3128,31 +3752,32 @@ packages:
resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
engines: {node: '>=16'}
- type-fest@5.3.0:
- resolution: {integrity: sha512-d9CwU93nN0IA1QL+GSNDdwLAu1Ew5ZjTwupvedwg3WdfoH6pIDvYQ2hV0Uc2nKBLPq7NB5apCx57MLS5qlmO5g==}
+ type-fest@5.4.4:
+ resolution: {integrity: sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw==}
engines: {node: '>=20'}
typed-array-buffer@1.0.3:
resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==}
engines: {node: '>= 0.4'}
- typeorm@0.3.28:
- resolution: {integrity: sha512-6GH7wXhtfq2D33ZuRXYwIsl/qM5685WZcODZb7noOOcRMteM9KF2x2ap3H0EBjnSV0VO4gNAfJT5Ukp0PkOlvg==}
+ typeorm@0.3.27:
+ resolution: {integrity: sha512-pNV1bn+1n8qEe8tUNsNdD8ejuPcMAg47u2lUGnbsajiNUr3p2Js1XLKQjBMH0yMRMDfdX8T+fIRejFmIwy9x4A==}
engines: {node: '>=16.13.0'}
hasBin: true
peerDependencies:
- '@google-cloud/spanner': ^5.18.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
+ '@google-cloud/spanner': ^5.18.0 || ^6.0.0 || ^7.0.0
'@sap/hana-client': ^2.14.22
better-sqlite3: ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0
ioredis: ^5.0.4
mongodb: ^5.8.0 || ^6.0.0
- mssql: ^9.1.1 || ^10.0.0 || ^11.0.0 || ^12.0.0
+ mssql: ^9.1.1 || ^10.0.1 || ^11.0.1
mysql2: ^2.2.5 || ^3.0.1
oracledb: ^6.3.0
pg: ^8.5.1
pg-native: ^3.0.0
pg-query-stream: ^4.0.0
redis: ^3.1.1 || ^4.0.0 || ^5.0.14
+ reflect-metadata: ^0.1.14 || ^0.2.0
sql.js: ^1.4.0
sqlite3: ^5.0.3
ts-node: ^10.7.0
@@ -3196,8 +3821,8 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
- ultracite@6.3.9:
- resolution: {integrity: sha512-bqj8Eh3VWYcjHXMR3+a9xqZXbgFY3PxwB+m9LTC+W1HHs5qtOtxH4QX51MiH513jfThpdM+cnWAk5KNltVh+Kg==}
+ ultracite@6.0.5:
+ resolution: {integrity: sha512-H5OrSBQzZmjxL1qwLgi21cmWI46i9sk0iBIVurb+iGOmr6XdzPANcydpOS3h4dN/dCzDGp29GOCqVaUGHeET1g==}
hasBin: true
undici-types@7.16.0:
@@ -3207,8 +3832,8 @@ packages:
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
engines: {node: '>= 10.0.0'}
- update-browserslist-db@1.2.2:
- resolution: {integrity: sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==}
+ update-browserslist-db@1.2.3:
+ resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
hasBin: true
peerDependencies:
browserslist: '>= 4.21.0'
@@ -3233,8 +3858,11 @@ packages:
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
engines: {node: '>= 8'}
- which-typed-array@1.1.19:
- resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==}
+ web-vitals@3.5.2:
+ resolution: {integrity: sha512-c0rhqNcHXRkY/ogGDJQxZ9Im9D19hDihbzSQJrsioex+KnFgmMzBiy57Z1EjkhX/+OjyBpclDCzz2ITtjokFmg==}
+
+ which-typed-array@1.1.20:
+ resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==}
engines: {node: '>= 0.4'}
which@2.0.2:
@@ -3256,18 +3884,6 @@ packages:
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
- ws@8.17.1:
- resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==}
- engines: {node: '>=10.0.0'}
- peerDependencies:
- bufferutil: ^4.0.1
- utf-8-validate: '>=5.0.2'
- peerDependenciesMeta:
- bufferutil:
- optional: true
- utf-8-validate:
- optional: true
-
ws@8.18.3:
resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==}
engines: {node: '>=10.0.0'}
@@ -3292,15 +3908,11 @@ packages:
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
engines: {node: '>=12'}
- yocto-queue@1.2.2:
- resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==}
- engines: {node: '>=12.20'}
+ zod@4.3.6:
+ resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
- zod@4.1.13:
- resolution: {integrity: sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==}
-
- zustand@5.0.9:
- resolution: {integrity: sha512-ALBtUj0AfjJt3uNRQoL1tL2tMvj6Gp/6e39dnfT6uzpelGru8v1tPOGBzayOWbPJvujM8JojDk3E1LxeFisBNg==}
+ zustand@5.0.11:
+ resolution: {integrity: sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==}
engines: {node: '>=12.20.0'}
peerDependencies:
'@types/react': '>=18.0.0'
@@ -3366,62 +3978,222 @@ snapshots:
'@babel/helper-validator-identifier@7.28.5': {}
- '@biomejs/biome@2.3.8':
+ '@biomejs/biome@2.3.2':
optionalDependencies:
- '@biomejs/cli-darwin-arm64': 2.3.8
- '@biomejs/cli-darwin-x64': 2.3.8
- '@biomejs/cli-linux-arm64': 2.3.8
- '@biomejs/cli-linux-arm64-musl': 2.3.8
- '@biomejs/cli-linux-x64': 2.3.8
- '@biomejs/cli-linux-x64-musl': 2.3.8
- '@biomejs/cli-win32-arm64': 2.3.8
- '@biomejs/cli-win32-x64': 2.3.8
+ '@biomejs/cli-darwin-arm64': 2.3.2
+ '@biomejs/cli-darwin-x64': 2.3.2
+ '@biomejs/cli-linux-arm64': 2.3.2
+ '@biomejs/cli-linux-arm64-musl': 2.3.2
+ '@biomejs/cli-linux-x64': 2.3.2
+ '@biomejs/cli-linux-x64-musl': 2.3.2
+ '@biomejs/cli-win32-arm64': 2.3.2
+ '@biomejs/cli-win32-x64': 2.3.2
- '@biomejs/cli-darwin-arm64@2.3.8':
+ '@biomejs/cli-darwin-arm64@2.3.2':
optional: true
- '@biomejs/cli-darwin-x64@2.3.8':
+ '@biomejs/cli-darwin-x64@2.3.2':
optional: true
- '@biomejs/cli-linux-arm64-musl@2.3.8':
+ '@biomejs/cli-linux-arm64-musl@2.3.2':
optional: true
- '@biomejs/cli-linux-arm64@2.3.8':
+ '@biomejs/cli-linux-arm64@2.3.2':
optional: true
- '@biomejs/cli-linux-x64-musl@2.3.8':
+ '@biomejs/cli-linux-x64-musl@2.3.2':
optional: true
- '@biomejs/cli-linux-x64@2.3.8':
+ '@biomejs/cli-linux-x64@2.3.2':
optional: true
- '@biomejs/cli-win32-arm64@2.3.8':
+ '@biomejs/cli-win32-arm64@2.3.2':
optional: true
- '@biomejs/cli-win32-x64@2.3.8':
+ '@biomejs/cli-win32-x64@2.3.2':
optional: true
- '@c15t/backend@1.8.0(@libsql/client@0.15.15)(@opentelemetry/instrumentation@0.208.0(@opentelemetry/api@1.9.0))(@types/better-sqlite3@7.6.13)(better-sqlite3@12.5.0)(typeorm@0.3.28(better-sqlite3@12.5.0))(ws@8.18.3)':
+ '@c15t/backend@1.7.1(@libsql/client@0.15.15)(@opentelemetry/instrumentation@0.207.0(@opentelemetry/api@1.9.0))(@types/better-sqlite3@7.6.13)(better-sqlite3@12.4.1)(typeorm@0.3.27(better-sqlite3@12.4.1)(reflect-metadata@0.2.2))(ws@8.18.3)':
+ dependencies:
+ '@c15t/logger': 1.0.0
+ '@c15t/translations': 1.7.0
+ '@opentelemetry/api': 1.9.0
+ '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0)
+ '@opentelemetry/sdk-node': 0.203.0(@opentelemetry/api@1.9.0)
+ '@opentelemetry/sdk-trace-base': 2.5.1(@opentelemetry/api@1.9.0)
+ '@orpc/contract': 1.8.1(@opentelemetry/api@1.9.0)
+ '@orpc/openapi': 1.8.1(@opentelemetry/api@1.9.0)(ws@8.18.3)
+ '@orpc/otel': 1.13.5(@opentelemetry/api@1.9.0)(@opentelemetry/instrumentation@0.207.0(@opentelemetry/api@1.9.0))
+ '@orpc/server': 1.8.1(@opentelemetry/api@1.9.0)(ws@8.18.3)
+ '@orpc/zod': 1.8.1(@opentelemetry/api@1.9.0)(@orpc/contract@1.8.1(@opentelemetry/api@1.9.0))(@orpc/server@1.8.1(@opentelemetry/api@1.9.0)(ws@8.18.3))(ws@8.18.3)(zod@4.3.6)
+ base-x: 5.0.1
+ defu: 6.1.4
+ drizzle-orm: 0.44.7(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.4.1)(kysely@0.27.6)
+ fumadb: 0.1.2(drizzle-orm@0.44.7(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.4.1)(kysely@0.27.6))(typeorm@0.3.27(better-sqlite3@12.4.1)(reflect-metadata@0.2.2))
+ kysely: 0.27.6
+ neverthrow: 8.2.0
+ superjson: 2.2.6
+ zod: 4.3.6
+ transitivePeerDependencies:
+ - '@aws-sdk/client-rds-data'
+ - '@cloudflare/workers-types'
+ - '@electric-sql/pglite'
+ - '@libsql/client'
+ - '@libsql/client-wasm'
+ - '@neondatabase/serverless'
+ - '@op-engineering/op-sqlite'
+ - '@opentelemetry/instrumentation'
+ - '@planetscale/database'
+ - '@prisma/client'
+ - '@tidbcloud/serverless'
+ - '@types/better-sqlite3'
+ - '@types/pg'
+ - '@types/sql.js'
+ - '@upstash/redis'
+ - '@vercel/postgres'
+ - '@xata.io/client'
+ - better-sqlite3
+ - bun-types
+ - convex
+ - crossws
+ - expo-sqlite
+ - gel
+ - knex
+ - mongodb
+ - mysql2
+ - pg
+ - postgres
+ - prisma
+ - sql.js
+ - sqlite3
+ - supports-color
+ - typeorm
+ - ws
+
+ '@c15t/backend@1.8.0(@libsql/client@0.15.15)(@opentelemetry/instrumentation@0.207.0(@opentelemetry/api@1.9.0))(@types/better-sqlite3@7.6.13)(better-sqlite3@12.4.1)(typeorm@0.3.27(better-sqlite3@12.4.1)(reflect-metadata@0.2.2))(ws@8.18.3)':
+ dependencies:
+ '@c15t/logger': 1.0.1
+ '@c15t/translations': 1.8.0
+ '@opentelemetry/api': 1.9.0
+ '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0)
+ '@opentelemetry/sdk-node': 0.203.0(@opentelemetry/api@1.9.0)
+ '@opentelemetry/sdk-trace-base': 2.5.1(@opentelemetry/api@1.9.0)
+ '@orpc/contract': 1.8.1(@opentelemetry/api@1.9.0)
+ '@orpc/openapi': 1.8.1(@opentelemetry/api@1.9.0)(ws@8.18.3)
+ '@orpc/otel': 1.13.5(@opentelemetry/api@1.9.0)(@opentelemetry/instrumentation@0.207.0(@opentelemetry/api@1.9.0))
+ '@orpc/server': 1.8.1(@opentelemetry/api@1.9.0)(ws@8.18.3)
+ '@orpc/zod': 1.8.1(@opentelemetry/api@1.9.0)(@orpc/contract@1.8.1(@opentelemetry/api@1.9.0))(@orpc/server@1.8.1(@opentelemetry/api@1.9.0)(ws@8.18.3))(ws@8.18.3)(zod@4.3.6)
+ base-x: 5.0.1
+ defu: 6.1.4
+ drizzle-orm: 0.44.7(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.4.1)(kysely@0.27.6)
+ fumadb: 0.1.2(drizzle-orm@0.44.7(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.4.1)(kysely@0.27.6))(typeorm@0.3.27(better-sqlite3@12.4.1)(reflect-metadata@0.2.2))
+ kysely: 0.27.6
+ neverthrow: 8.2.0
+ superjson: 2.2.6
+ zod: 4.3.6
+ transitivePeerDependencies:
+ - '@aws-sdk/client-rds-data'
+ - '@cloudflare/workers-types'
+ - '@electric-sql/pglite'
+ - '@libsql/client'
+ - '@libsql/client-wasm'
+ - '@neondatabase/serverless'
+ - '@op-engineering/op-sqlite'
+ - '@opentelemetry/instrumentation'
+ - '@planetscale/database'
+ - '@prisma/client'
+ - '@tidbcloud/serverless'
+ - '@types/better-sqlite3'
+ - '@types/pg'
+ - '@types/sql.js'
+ - '@upstash/redis'
+ - '@vercel/postgres'
+ - '@xata.io/client'
+ - better-sqlite3
+ - bun-types
+ - convex
+ - crossws
+ - expo-sqlite
+ - gel
+ - knex
+ - mongodb
+ - mysql2
+ - pg
+ - postgres
+ - prisma
+ - sql.js
+ - sqlite3
+ - supports-color
+ - typeorm
+ - ws
+
+ '@c15t/logger@1.0.0':
+ dependencies:
+ chalk: 5.6.2
+ neverthrow: 8.2.0
+ picocolors: 1.1.1
+
+ '@c15t/logger@1.0.1':
+ dependencies:
+ chalk: 5.6.2
+ neverthrow: 8.2.0
+ picocolors: 1.1.1
+
+ '@c15t/nextjs@1.7.1(@opentelemetry/api@1.9.0)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(next@16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(ws@8.18.3)':
+ dependencies:
+ '@c15t/react': 1.7.1(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(@opentelemetry/instrumentation@0.207.0(@opentelemetry/api@1.9.0))(@types/better-sqlite3@7.6.13)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(better-sqlite3@12.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typeorm@0.3.27(better-sqlite3@12.4.1)(reflect-metadata@0.2.2))(use-sync-external-store@1.6.0(react@19.2.4))(ws@8.18.3)
+ '@c15t/translations': 1.7.0
+ next: 16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ transitivePeerDependencies:
+ - '@aws-sdk/client-rds-data'
+ - '@cloudflare/workers-types'
+ - '@electric-sql/pglite'
+ - '@libsql/client'
+ - '@libsql/client-wasm'
+ - '@neondatabase/serverless'
+ - '@op-engineering/op-sqlite'
+ - '@opentelemetry/api'
+ - '@opentelemetry/instrumentation'
+ - '@planetscale/database'
+ - '@prisma/client'
+ - '@tidbcloud/serverless'
+ - '@types/better-sqlite3'
+ - '@types/pg'
+ - '@types/react'
+ - '@types/react-dom'
+ - '@types/sql.js'
+ - '@upstash/redis'
+ - '@vercel/postgres'
+ - '@xata.io/client'
+ - better-sqlite3
+ - bun-types
+ - convex
+ - crossws
+ - expo-sqlite
+ - gel
+ - immer
+ - knex
+ - mongodb
+ - mysql2
+ - pg
+ - postgres
+ - prisma
+ - sql.js
+ - sqlite3
+ - supports-color
+ - typeorm
+ - use-sync-external-store
+ - ws
+
+ '@c15t/nextjs@1.8.1(@opentelemetry/api@1.9.0)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(next@16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(ws@8.18.3)':
dependencies:
- '@c15t/logger': 1.0.1
+ '@c15t/react': 1.8.1(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(@opentelemetry/instrumentation@0.207.0(@opentelemetry/api@1.9.0))(@types/better-sqlite3@7.6.13)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(better-sqlite3@12.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typeorm@0.3.27(better-sqlite3@12.4.1)(reflect-metadata@0.2.2))(use-sync-external-store@1.6.0(react@19.2.4))(ws@8.18.3)
'@c15t/translations': 1.8.0
- '@opentelemetry/api': 1.9.0
- '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0)
- '@opentelemetry/sdk-node': 0.203.0(@opentelemetry/api@1.9.0)
- '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0)
- '@orpc/contract': 1.8.1(@opentelemetry/api@1.9.0)
- '@orpc/openapi': 1.8.1(@opentelemetry/api@1.9.0)(ws@8.18.3)
- '@orpc/otel': 1.12.2(@opentelemetry/api@1.9.0)(@opentelemetry/instrumentation@0.208.0(@opentelemetry/api@1.9.0))
- '@orpc/server': 1.8.1(@opentelemetry/api@1.9.0)(ws@8.18.3)
- '@orpc/zod': 1.8.1(@opentelemetry/api@1.9.0)(@orpc/contract@1.8.1(@opentelemetry/api@1.9.0))(@orpc/server@1.8.1(@opentelemetry/api@1.9.0)(ws@8.18.3))(ws@8.18.3)(zod@4.1.13)
- base-x: 5.0.1
- defu: 6.1.4
- drizzle-orm: 0.44.7(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.5.0)(kysely@0.27.6)
- fumadb: 0.1.2(drizzle-orm@0.44.7(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.5.0)(kysely@0.27.6))(typeorm@0.3.28(better-sqlite3@12.5.0))
- kysely: 0.27.6
- neverthrow: 8.2.0
- superjson: 2.2.6
- zod: 4.1.13
+ next: 16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
transitivePeerDependencies:
- '@aws-sdk/client-rds-data'
- '@cloudflare/workers-types'
@@ -3430,12 +4202,15 @@ snapshots:
- '@libsql/client-wasm'
- '@neondatabase/serverless'
- '@op-engineering/op-sqlite'
+ - '@opentelemetry/api'
- '@opentelemetry/instrumentation'
- '@planetscale/database'
- '@prisma/client'
- '@tidbcloud/serverless'
- '@types/better-sqlite3'
- '@types/pg'
+ - '@types/react'
+ - '@types/react-dom'
- '@types/sql.js'
- '@upstash/redis'
- '@vercel/postgres'
@@ -3446,6 +4221,7 @@ snapshots:
- crossws
- expo-sqlite
- gel
+ - immer
- knex
- mongodb
- mysql2
@@ -3456,21 +4232,19 @@ snapshots:
- sqlite3
- supports-color
- typeorm
+ - use-sync-external-store
- ws
- '@c15t/logger@1.0.1':
- dependencies:
- chalk: 5.6.2
- neverthrow: 8.2.0
- picocolors: 1.1.1
-
- '@c15t/nextjs@1.8.1(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(@opentelemetry/instrumentation@0.208.0(@opentelemetry/api@1.9.0))(@types/better-sqlite3@7.6.13)(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(better-sqlite3@12.5.0)(next@16.0.7(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typeorm@0.3.28(better-sqlite3@12.5.0))(use-sync-external-store@1.6.0(react@19.2.1))(ws@8.18.3)':
+ '@c15t/react@1.7.1(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(@opentelemetry/instrumentation@0.207.0(@opentelemetry/api@1.9.0))(@types/better-sqlite3@7.6.13)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(better-sqlite3@12.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typeorm@0.3.27(better-sqlite3@12.4.1)(reflect-metadata@0.2.2))(use-sync-external-store@1.6.0(react@19.2.4))(ws@8.18.3)':
dependencies:
- '@c15t/react': 1.8.1(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(@opentelemetry/instrumentation@0.208.0(@opentelemetry/api@1.9.0))(@types/better-sqlite3@7.6.13)(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(better-sqlite3@12.5.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typeorm@0.3.28(better-sqlite3@12.5.0))(use-sync-external-store@1.6.0(react@19.2.1))(ws@8.18.3)
- '@c15t/translations': 1.8.0
- next: 16.0.7(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
- react: 19.2.1
- react-dom: 19.2.1(react@19.2.1)
+ '@radix-ui/react-accordion': 1.2.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-slot': 1.2.0(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-switch': 1.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ c15t: 1.7.1(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(@opentelemetry/instrumentation@0.207.0(@opentelemetry/api@1.9.0))(@types/better-sqlite3@7.6.13)(@types/react@19.2.14)(better-sqlite3@12.4.1)(react@19.2.4)(typeorm@0.3.27(better-sqlite3@12.4.1)(reflect-metadata@0.2.2))(use-sync-external-store@1.6.0(react@19.2.4))(ws@8.18.3)
+ clsx: 2.1.1
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ zustand: 5.0.11(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))
transitivePeerDependencies:
- '@aws-sdk/client-rds-data'
- '@cloudflare/workers-types'
@@ -3512,16 +4286,16 @@ snapshots:
- use-sync-external-store
- ws
- '@c15t/react@1.8.1(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(@opentelemetry/instrumentation@0.208.0(@opentelemetry/api@1.9.0))(@types/better-sqlite3@7.6.13)(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(better-sqlite3@12.5.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typeorm@0.3.28(better-sqlite3@12.5.0))(use-sync-external-store@1.6.0(react@19.2.1))(ws@8.18.3)':
+ '@c15t/react@1.8.1(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(@opentelemetry/instrumentation@0.207.0(@opentelemetry/api@1.9.0))(@types/better-sqlite3@7.6.13)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(better-sqlite3@12.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typeorm@0.3.27(better-sqlite3@12.4.1)(reflect-metadata@0.2.2))(use-sync-external-store@1.6.0(react@19.2.4))(ws@8.18.3)':
dependencies:
- '@radix-ui/react-accordion': 1.2.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
- '@radix-ui/react-slot': 1.2.0(@types/react@19.2.7)(react@19.2.1)
- '@radix-ui/react-switch': 1.1.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
- c15t: 1.8.1(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(@opentelemetry/instrumentation@0.208.0(@opentelemetry/api@1.9.0))(@types/better-sqlite3@7.6.13)(@types/react@19.2.7)(better-sqlite3@12.5.0)(react@19.2.1)(typeorm@0.3.28(better-sqlite3@12.5.0))(use-sync-external-store@1.6.0(react@19.2.1))(ws@8.18.3)
+ '@radix-ui/react-accordion': 1.2.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-slot': 1.2.0(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-switch': 1.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ c15t: 1.8.1(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(@opentelemetry/instrumentation@0.207.0(@opentelemetry/api@1.9.0))(@types/better-sqlite3@7.6.13)(@types/react@19.2.14)(better-sqlite3@12.4.1)(react@19.2.4)(typeorm@0.3.27(better-sqlite3@12.4.1)(reflect-metadata@0.2.2))(use-sync-external-store@1.6.0(react@19.2.4))(ws@8.18.3)
clsx: 2.1.1
- react: 19.2.1
- react-dom: 19.2.1(react@19.2.1)
- zustand: 5.0.9(@types/react@19.2.7)(react@19.2.1)(use-sync-external-store@1.6.0(react@19.2.1))
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ zustand: 5.0.11(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))
transitivePeerDependencies:
- '@aws-sdk/client-rds-data'
- '@cloudflare/workers-types'
@@ -3563,6 +4337,8 @@ snapshots:
- use-sync-external-store
- ws
+ '@c15t/translations@1.7.0': {}
+
'@c15t/translations@1.8.0': {}
'@clack/core@0.5.0':
@@ -3575,6 +4351,11 @@ snapshots:
picocolors: 1.1.1
sisteransi: 1.0.5
+ '@clack/core@1.0.1':
+ dependencies:
+ picocolors: 1.1.1
+ sisteransi: 1.0.5
+
'@clack/prompts@0.11.0':
dependencies:
'@clack/core': 0.5.0
@@ -3587,23 +4368,29 @@ snapshots:
picocolors: 1.1.1
sisteransi: 1.0.5
+ '@clack/prompts@1.0.1':
+ dependencies:
+ '@clack/core': 1.0.1
+ picocolors: 1.1.1
+ sisteransi: 1.0.5
+
'@colors/colors@1.5.0':
optional: true
- '@didomi/react@1.8.8(react@19.2.1)':
+ '@didomi/react@1.8.8(react@19.2.4)':
dependencies:
prop-types: 15.8.1
- react: 19.2.1
+ react: 19.2.4
'@drizzle-team/brocli@0.10.2': {}
- '@emnapi/core@1.7.1':
+ '@emnapi/core@1.8.1':
dependencies:
'@emnapi/wasi-threads': 1.1.0
tslib: 2.8.1
optional: true
- '@emnapi/runtime@1.7.1':
+ '@emnapi/runtime@1.8.1':
dependencies:
tslib: 2.8.1
optional: true
@@ -3621,7 +4408,7 @@ snapshots:
'@esbuild-kit/esm-loader@2.6.5':
dependencies:
'@esbuild-kit/core-utils': 3.3.2
- get-tsconfig: 4.13.0
+ get-tsconfig: 4.13.6
'@esbuild/aix-ppc64@0.25.12':
optional: true
@@ -3767,7 +4554,7 @@ snapshots:
'@esbuild/win32-x64@0.25.12':
optional: true
- '@grpc/grpc-js@1.14.2':
+ '@grpc/grpc-js@1.14.3':
dependencies:
'@grpc/proto-loader': 0.8.0
'@js-sdsl/ordered-map': 4.4.2
@@ -3864,7 +4651,7 @@ snapshots:
'@img/sharp-wasm32@0.34.5':
dependencies:
- '@emnapi/runtime': 1.7.1
+ '@emnapi/runtime': 1.8.1
optional: true
'@img/sharp-win32-arm64@0.34.5':
@@ -3876,12 +4663,6 @@ snapshots:
'@img/sharp-win32-x64@0.34.5':
optional: true
- '@isaacs/balanced-match@4.0.1': {}
-
- '@isaacs/brace-expansion@5.0.0':
- dependencies:
- '@isaacs/balanced-match': 4.0.1
-
'@isaacs/cliui@8.0.2':
dependencies:
string-width: 5.1.2
@@ -3960,65 +4741,90 @@ snapshots:
'@libsql/win32-x64-msvc@0.5.22':
optional: true
- '@module-federation/error-codes@0.21.6': {}
+ '@module-federation/error-codes@0.21.1': {}
- '@module-federation/runtime-core@0.21.6':
+ '@module-federation/error-codes@0.22.0': {}
+
+ '@module-federation/runtime-core@0.21.1':
+ dependencies:
+ '@module-federation/error-codes': 0.21.1
+ '@module-federation/sdk': 0.21.1
+
+ '@module-federation/runtime-core@0.22.0':
+ dependencies:
+ '@module-federation/error-codes': 0.22.0
+ '@module-federation/sdk': 0.22.0
+
+ '@module-federation/runtime-tools@0.21.1':
+ dependencies:
+ '@module-federation/runtime': 0.21.1
+ '@module-federation/webpack-bundler-runtime': 0.21.1
+
+ '@module-federation/runtime-tools@0.22.0':
dependencies:
- '@module-federation/error-codes': 0.21.6
- '@module-federation/sdk': 0.21.6
+ '@module-federation/runtime': 0.22.0
+ '@module-federation/webpack-bundler-runtime': 0.22.0
- '@module-federation/runtime-tools@0.21.6':
+ '@module-federation/runtime@0.21.1':
dependencies:
- '@module-federation/runtime': 0.21.6
- '@module-federation/webpack-bundler-runtime': 0.21.6
+ '@module-federation/error-codes': 0.21.1
+ '@module-federation/runtime-core': 0.21.1
+ '@module-federation/sdk': 0.21.1
- '@module-federation/runtime@0.21.6':
+ '@module-federation/runtime@0.22.0':
dependencies:
- '@module-federation/error-codes': 0.21.6
- '@module-federation/runtime-core': 0.21.6
- '@module-federation/sdk': 0.21.6
+ '@module-federation/error-codes': 0.22.0
+ '@module-federation/runtime-core': 0.22.0
+ '@module-federation/sdk': 0.22.0
+
+ '@module-federation/sdk@0.21.1': {}
- '@module-federation/sdk@0.21.6': {}
+ '@module-federation/sdk@0.22.0': {}
- '@module-federation/webpack-bundler-runtime@0.21.6':
+ '@module-federation/webpack-bundler-runtime@0.21.1':
dependencies:
- '@module-federation/runtime': 0.21.6
- '@module-federation/sdk': 0.21.6
+ '@module-federation/runtime': 0.21.1
+ '@module-federation/sdk': 0.21.1
+
+ '@module-federation/webpack-bundler-runtime@0.22.0':
+ dependencies:
+ '@module-federation/runtime': 0.22.0
+ '@module-federation/sdk': 0.22.0
'@napi-rs/wasm-runtime@1.0.7':
dependencies:
- '@emnapi/core': 1.7.1
- '@emnapi/runtime': 1.7.1
+ '@emnapi/core': 1.8.1
+ '@emnapi/runtime': 1.8.1
'@tybys/wasm-util': 0.10.1
optional: true
'@neon-rs/load@0.0.4':
optional: true
- '@next/env@16.0.7': {}
+ '@next/env@16.1.6': {}
- '@next/swc-darwin-arm64@16.0.7':
+ '@next/swc-darwin-arm64@16.1.6':
optional: true
- '@next/swc-darwin-x64@16.0.7':
+ '@next/swc-darwin-x64@16.1.6':
optional: true
- '@next/swc-linux-arm64-gnu@16.0.7':
+ '@next/swc-linux-arm64-gnu@16.1.6':
optional: true
- '@next/swc-linux-arm64-musl@16.0.7':
+ '@next/swc-linux-arm64-musl@16.1.6':
optional: true
- '@next/swc-linux-x64-gnu@16.0.7':
+ '@next/swc-linux-x64-gnu@16.1.6':
optional: true
- '@next/swc-linux-x64-musl@16.0.7':
+ '@next/swc-linux-x64-musl@16.1.6':
optional: true
- '@next/swc-win32-arm64-msvc@16.0.7':
+ '@next/swc-win32-arm64-msvc@16.1.6':
optional: true
- '@next/swc-win32-x64-msvc@16.0.7':
+ '@next/swc-win32-x64-msvc@16.1.6':
optional: true
'@noble/hashes@1.8.0': {}
@@ -4027,7 +4833,7 @@ snapshots:
dependencies:
'@opentelemetry/api': 1.9.0
- '@opentelemetry/api-logs@0.208.0':
+ '@opentelemetry/api-logs@0.207.0':
dependencies:
'@opentelemetry/api': 1.9.0
@@ -4040,16 +4846,16 @@ snapshots:
'@opentelemetry/core@2.0.1(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
- '@opentelemetry/semantic-conventions': 1.38.0
+ '@opentelemetry/semantic-conventions': 1.39.0
- '@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0)':
+ '@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
- '@opentelemetry/semantic-conventions': 1.38.0
+ '@opentelemetry/semantic-conventions': 1.39.0
'@opentelemetry/exporter-logs-otlp-grpc@0.203.0(@opentelemetry/api@1.9.0)':
dependencies:
- '@grpc/grpc-js': 1.14.2
+ '@grpc/grpc-js': 1.14.3
'@opentelemetry/api': 1.9.0
'@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0)
'@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0)
@@ -4079,7 +4885,7 @@ snapshots:
'@opentelemetry/exporter-metrics-otlp-grpc@0.203.0(@opentelemetry/api@1.9.0)':
dependencies:
- '@grpc/grpc-js': 1.14.2
+ '@grpc/grpc-js': 1.14.3
'@opentelemetry/api': 1.9.0
'@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0)
'@opentelemetry/exporter-metrics-otlp-http': 0.203.0(@opentelemetry/api@1.9.0)
@@ -4117,7 +4923,7 @@ snapshots:
'@opentelemetry/exporter-trace-otlp-grpc@0.203.0(@opentelemetry/api@1.9.0)':
dependencies:
- '@grpc/grpc-js': 1.14.2
+ '@grpc/grpc-js': 1.14.3
'@opentelemetry/api': 1.9.0
'@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0)
'@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0)
@@ -4150,7 +4956,7 @@ snapshots:
'@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0)
'@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0)
- '@opentelemetry/semantic-conventions': 1.38.0
+ '@opentelemetry/semantic-conventions': 1.39.0
'@opentelemetry/instrumentation@0.203.0(@opentelemetry/api@1.9.0)':
dependencies:
@@ -4161,11 +4967,11 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@opentelemetry/instrumentation@0.208.0(@opentelemetry/api@1.9.0)':
+ '@opentelemetry/instrumentation@0.207.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
- '@opentelemetry/api-logs': 0.208.0
- import-in-the-middle: 2.0.0
+ '@opentelemetry/api-logs': 0.207.0
+ import-in-the-middle: 2.0.6
require-in-the-middle: 8.0.1
transitivePeerDependencies:
- supports-color
@@ -4178,7 +4984,7 @@ snapshots:
'@opentelemetry/otlp-grpc-exporter-base@0.203.0(@opentelemetry/api@1.9.0)':
dependencies:
- '@grpc/grpc-js': 1.14.2
+ '@grpc/grpc-js': 1.14.3
'@opentelemetry/api': 1.9.0
'@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0)
'@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0)
@@ -4209,13 +5015,13 @@ snapshots:
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0)
- '@opentelemetry/semantic-conventions': 1.38.0
+ '@opentelemetry/semantic-conventions': 1.39.0
- '@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.0)':
+ '@opentelemetry/resources@2.5.1(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
- '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0)
- '@opentelemetry/semantic-conventions': 1.38.0
+ '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0)
+ '@opentelemetry/semantic-conventions': 1.39.0
'@opentelemetry/sdk-logs@0.203.0(@opentelemetry/api@1.9.0)':
dependencies:
@@ -4254,7 +5060,7 @@ snapshots:
'@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-trace-node': 2.0.1(@opentelemetry/api@1.9.0)
- '@opentelemetry/semantic-conventions': 1.38.0
+ '@opentelemetry/semantic-conventions': 1.39.0
transitivePeerDependencies:
- supports-color
@@ -4263,14 +5069,14 @@ snapshots:
'@opentelemetry/api': 1.9.0
'@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0)
'@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0)
- '@opentelemetry/semantic-conventions': 1.38.0
+ '@opentelemetry/semantic-conventions': 1.39.0
- '@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0)':
+ '@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
- '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0)
- '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0)
- '@opentelemetry/semantic-conventions': 1.38.0
+ '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0)
+ '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0)
+ '@opentelemetry/semantic-conventions': 1.39.0
'@opentelemetry/sdk-trace-node@2.0.1(@opentelemetry/api@1.9.0)':
dependencies:
@@ -4279,17 +5085,7 @@ snapshots:
'@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0)
- '@opentelemetry/semantic-conventions@1.38.0': {}
-
- '@orpc/client@1.12.2(@opentelemetry/api@1.9.0)':
- dependencies:
- '@orpc/shared': 1.12.2(@opentelemetry/api@1.9.0)
- '@orpc/standard-server': 1.12.2(@opentelemetry/api@1.9.0)
- '@orpc/standard-server-fetch': 1.12.2(@opentelemetry/api@1.9.0)
- '@orpc/standard-server-peer': 1.12.2(@opentelemetry/api@1.9.0)
- transitivePeerDependencies:
- - '@opentelemetry/api'
- optional: true
+ '@opentelemetry/semantic-conventions@1.39.0': {}
'@orpc/client@1.8.1(@opentelemetry/api@1.9.0)':
dependencies:
@@ -4300,28 +5096,15 @@ snapshots:
transitivePeerDependencies:
- '@opentelemetry/api'
- '@orpc/contract@1.12.2(@opentelemetry/api@1.9.0)':
- dependencies:
- '@orpc/client': 1.12.2(@opentelemetry/api@1.9.0)
- '@orpc/shared': 1.12.2(@opentelemetry/api@1.9.0)
- '@standard-schema/spec': 1.0.0
- openapi-types: 12.1.3
- transitivePeerDependencies:
- - '@opentelemetry/api'
- optional: true
-
'@orpc/contract@1.8.1(@opentelemetry/api@1.9.0)':
dependencies:
'@orpc/client': 1.8.1(@opentelemetry/api@1.9.0)
'@orpc/shared': 1.8.1(@opentelemetry/api@1.9.0)
- '@standard-schema/spec': 1.0.0
+ '@standard-schema/spec': 1.1.0
openapi-types: 12.1.3
transitivePeerDependencies:
- '@opentelemetry/api'
- '@orpc/interop@1.12.2':
- optional: true
-
'@orpc/interop@1.8.1': {}
'@orpc/json-schema@1.8.1(@opentelemetry/api@1.9.0)(ws@8.18.3)':
@@ -4354,37 +5137,17 @@ snapshots:
'@orpc/server': 1.8.1(@opentelemetry/api@1.9.0)(ws@8.18.3)
'@orpc/shared': 1.8.1(@opentelemetry/api@1.9.0)
'@orpc/standard-server': 1.8.1(@opentelemetry/api@1.9.0)
- rou3: 0.7.10
+ rou3: 0.7.12
transitivePeerDependencies:
- '@opentelemetry/api'
- crossws
- ws
- '@orpc/otel@1.12.2(@opentelemetry/api@1.9.0)(@opentelemetry/instrumentation@0.208.0(@opentelemetry/api@1.9.0))':
+ '@orpc/otel@1.13.5(@opentelemetry/api@1.9.0)(@opentelemetry/instrumentation@0.207.0(@opentelemetry/api@1.9.0))':
dependencies:
'@opentelemetry/api': 1.9.0
- '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0)
- '@orpc/shared': 1.12.2(@opentelemetry/api@1.9.0)
-
- '@orpc/server@1.12.2(@opentelemetry/api@1.9.0)(ws@8.18.3)':
- dependencies:
- '@orpc/client': 1.12.2(@opentelemetry/api@1.9.0)
- '@orpc/contract': 1.12.2(@opentelemetry/api@1.9.0)
- '@orpc/interop': 1.12.2
- '@orpc/shared': 1.12.2(@opentelemetry/api@1.9.0)
- '@orpc/standard-server': 1.12.2(@opentelemetry/api@1.9.0)
- '@orpc/standard-server-aws-lambda': 1.12.2(@opentelemetry/api@1.9.0)
- '@orpc/standard-server-fastify': 1.12.2(@opentelemetry/api@1.9.0)
- '@orpc/standard-server-fetch': 1.12.2(@opentelemetry/api@1.9.0)
- '@orpc/standard-server-node': 1.12.2(@opentelemetry/api@1.9.0)
- '@orpc/standard-server-peer': 1.12.2(@opentelemetry/api@1.9.0)
- cookie: 1.1.1
- optionalDependencies:
- ws: 8.18.3
- transitivePeerDependencies:
- - '@opentelemetry/api'
- - fastify
- optional: true
+ '@opentelemetry/instrumentation': 0.207.0(@opentelemetry/api@1.9.0)
+ '@orpc/shared': 1.13.5(@opentelemetry/api@1.9.0)
'@orpc/server@1.8.1(@opentelemetry/api@1.9.0)(ws@8.18.3)':
dependencies:
@@ -4403,10 +5166,10 @@ snapshots:
transitivePeerDependencies:
- '@opentelemetry/api'
- '@orpc/shared@1.12.2(@opentelemetry/api@1.9.0)':
+ '@orpc/shared@1.13.5(@opentelemetry/api@1.9.0)':
dependencies:
radash: 12.1.1
- type-fest: 5.3.0
+ type-fest: 5.4.4
optionalDependencies:
'@opentelemetry/api': 1.9.0
@@ -4417,16 +5180,6 @@ snapshots:
optionalDependencies:
'@opentelemetry/api': 1.9.0
- '@orpc/standard-server-aws-lambda@1.12.2(@opentelemetry/api@1.9.0)':
- dependencies:
- '@orpc/shared': 1.12.2(@opentelemetry/api@1.9.0)
- '@orpc/standard-server': 1.12.2(@opentelemetry/api@1.9.0)
- '@orpc/standard-server-fetch': 1.12.2(@opentelemetry/api@1.9.0)
- '@orpc/standard-server-node': 1.12.2(@opentelemetry/api@1.9.0)
- transitivePeerDependencies:
- - '@opentelemetry/api'
- optional: true
-
'@orpc/standard-server-aws-lambda@1.8.1(@opentelemetry/api@1.9.0)':
dependencies:
'@orpc/shared': 1.8.1(@opentelemetry/api@1.9.0)
@@ -4436,23 +5189,6 @@ snapshots:
transitivePeerDependencies:
- '@opentelemetry/api'
- '@orpc/standard-server-fastify@1.12.2(@opentelemetry/api@1.9.0)':
- dependencies:
- '@orpc/shared': 1.12.2(@opentelemetry/api@1.9.0)
- '@orpc/standard-server': 1.12.2(@opentelemetry/api@1.9.0)
- '@orpc/standard-server-node': 1.12.2(@opentelemetry/api@1.9.0)
- transitivePeerDependencies:
- - '@opentelemetry/api'
- optional: true
-
- '@orpc/standard-server-fetch@1.12.2(@opentelemetry/api@1.9.0)':
- dependencies:
- '@orpc/shared': 1.12.2(@opentelemetry/api@1.9.0)
- '@orpc/standard-server': 1.12.2(@opentelemetry/api@1.9.0)
- transitivePeerDependencies:
- - '@opentelemetry/api'
- optional: true
-
'@orpc/standard-server-fetch@1.8.1(@opentelemetry/api@1.9.0)':
dependencies:
'@orpc/shared': 1.8.1(@opentelemetry/api@1.9.0)
@@ -4460,15 +5196,6 @@ snapshots:
transitivePeerDependencies:
- '@opentelemetry/api'
- '@orpc/standard-server-node@1.12.2(@opentelemetry/api@1.9.0)':
- dependencies:
- '@orpc/shared': 1.12.2(@opentelemetry/api@1.9.0)
- '@orpc/standard-server': 1.12.2(@opentelemetry/api@1.9.0)
- '@orpc/standard-server-fetch': 1.12.2(@opentelemetry/api@1.9.0)
- transitivePeerDependencies:
- - '@opentelemetry/api'
- optional: true
-
'@orpc/standard-server-node@1.8.1(@opentelemetry/api@1.9.0)':
dependencies:
'@orpc/shared': 1.8.1(@opentelemetry/api@1.9.0)
@@ -4477,14 +5204,6 @@ snapshots:
transitivePeerDependencies:
- '@opentelemetry/api'
- '@orpc/standard-server-peer@1.12.2(@opentelemetry/api@1.9.0)':
- dependencies:
- '@orpc/shared': 1.12.2(@opentelemetry/api@1.9.0)
- '@orpc/standard-server': 1.12.2(@opentelemetry/api@1.9.0)
- transitivePeerDependencies:
- - '@opentelemetry/api'
- optional: true
-
'@orpc/standard-server-peer@1.8.1(@opentelemetry/api@1.9.0)':
dependencies:
'@orpc/shared': 1.8.1(@opentelemetry/api@1.9.0)
@@ -4492,20 +5211,13 @@ snapshots:
transitivePeerDependencies:
- '@opentelemetry/api'
- '@orpc/standard-server@1.12.2(@opentelemetry/api@1.9.0)':
- dependencies:
- '@orpc/shared': 1.12.2(@opentelemetry/api@1.9.0)
- transitivePeerDependencies:
- - '@opentelemetry/api'
- optional: true
-
'@orpc/standard-server@1.8.1(@opentelemetry/api@1.9.0)':
dependencies:
'@orpc/shared': 1.8.1(@opentelemetry/api@1.9.0)
transitivePeerDependencies:
- '@opentelemetry/api'
- '@orpc/zod@1.8.1(@opentelemetry/api@1.9.0)(@orpc/contract@1.8.1(@opentelemetry/api@1.9.0))(@orpc/server@1.8.1(@opentelemetry/api@1.9.0)(ws@8.18.3))(ws@8.18.3)(zod@4.1.13)':
+ '@orpc/zod@1.8.1(@opentelemetry/api@1.9.0)(@orpc/contract@1.8.1(@opentelemetry/api@1.9.0))(@orpc/server@1.8.1(@opentelemetry/api@1.9.0)(ws@8.18.3))(ws@8.18.3)(zod@4.3.6)':
dependencies:
'@orpc/contract': 1.8.1(@opentelemetry/api@1.9.0)
'@orpc/json-schema': 1.8.1(@opentelemetry/api@1.9.0)(ws@8.18.3)
@@ -4514,7 +5226,7 @@ snapshots:
'@orpc/shared': 1.8.1(@opentelemetry/api@1.9.0)
escape-string-regexp: 5.0.0
wildcard-match: 5.1.4
- zod: 4.1.13
+ zod: 4.3.6
transitivePeerDependencies:
- '@opentelemetry/api'
- crossws
@@ -4527,9 +5239,9 @@ snapshots:
'@pkgjs/parseargs@0.11.0':
optional: true
- '@playwright/test@1.57.0':
+ '@playwright/test@1.58.2':
dependencies:
- playwright: 1.57.0
+ playwright: 1.58.2
'@protobufjs/aspromise@1.1.2': {}
@@ -4556,185 +5268,225 @@ snapshots:
'@radix-ui/primitive@1.1.2': {}
- '@radix-ui/react-accordion@1.2.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)':
+ '@radix-ui/react-accordion@1.2.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@radix-ui/primitive': 1.1.2
- '@radix-ui/react-collapsible': 1.1.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
- '@radix-ui/react-collection': 1.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.1)
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.1)
- '@radix-ui/react-direction': 1.1.1(@types/react@19.2.7)(react@19.2.1)
- '@radix-ui/react-id': 1.1.1(@types/react@19.2.7)(react@19.2.1)
- '@radix-ui/react-primitive': 2.0.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
- '@radix-ui/react-use-controllable-state': 1.1.1(@types/react@19.2.7)(react@19.2.1)
- react: 19.2.1
- react-dom: 19.2.1(react@19.2.1)
+ '@radix-ui/react-collapsible': 1.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-collection': 1.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-primitive': 2.0.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-use-controllable-state': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
optionalDependencies:
- '@types/react': 19.2.7
- '@types/react-dom': 19.2.3(@types/react@19.2.7)
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
- '@radix-ui/react-collapsible@1.1.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)':
+ '@radix-ui/react-collapsible@1.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@radix-ui/primitive': 1.1.2
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.1)
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.1)
- '@radix-ui/react-id': 1.1.1(@types/react@19.2.7)(react@19.2.1)
- '@radix-ui/react-presence': 1.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
- '@radix-ui/react-primitive': 2.0.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
- '@radix-ui/react-use-controllable-state': 1.1.1(@types/react@19.2.7)(react@19.2.1)
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.1)
- react: 19.2.1
- react-dom: 19.2.1(react@19.2.1)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-presence': 1.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-primitive': 2.0.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-use-controllable-state': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
optionalDependencies:
- '@types/react': 19.2.7
- '@types/react-dom': 19.2.3(@types/react@19.2.7)
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
- '@radix-ui/react-collection@1.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)':
+ '@radix-ui/react-collection@1.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.1)
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.1)
- '@radix-ui/react-primitive': 2.0.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
- '@radix-ui/react-slot': 1.2.0(@types/react@19.2.7)(react@19.2.1)
- react: 19.2.1
- react-dom: 19.2.1(react@19.2.1)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-primitive': 2.0.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-slot': 1.2.0(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
optionalDependencies:
- '@types/react': 19.2.7
- '@types/react-dom': 19.2.3(@types/react@19.2.7)
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
- '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.7)(react@19.2.1)':
+ '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.2.4)':
dependencies:
- react: 19.2.1
+ react: 19.2.4
optionalDependencies:
- '@types/react': 19.2.7
+ '@types/react': 19.2.14
- '@radix-ui/react-context@1.1.2(@types/react@19.2.7)(react@19.2.1)':
+ '@radix-ui/react-context@1.1.2(@types/react@19.2.14)(react@19.2.4)':
dependencies:
- react: 19.2.1
+ react: 19.2.4
optionalDependencies:
- '@types/react': 19.2.7
+ '@types/react': 19.2.14
- '@radix-ui/react-direction@1.1.1(@types/react@19.2.7)(react@19.2.1)':
+ '@radix-ui/react-direction@1.1.1(@types/react@19.2.14)(react@19.2.4)':
dependencies:
- react: 19.2.1
+ react: 19.2.4
optionalDependencies:
- '@types/react': 19.2.7
+ '@types/react': 19.2.14
- '@radix-ui/react-id@1.1.1(@types/react@19.2.7)(react@19.2.1)':
+ '@radix-ui/react-id@1.1.1(@types/react@19.2.14)(react@19.2.4)':
dependencies:
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.1)
- react: 19.2.1
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
optionalDependencies:
- '@types/react': 19.2.7
+ '@types/react': 19.2.14
- '@radix-ui/react-presence@1.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)':
+ '@radix-ui/react-presence@1.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.1)
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.1)
- react: 19.2.1
- react-dom: 19.2.1(react@19.2.1)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
optionalDependencies:
- '@types/react': 19.2.7
- '@types/react-dom': 19.2.3(@types/react@19.2.7)
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
- '@radix-ui/react-primitive@2.0.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)':
+ '@radix-ui/react-primitive@2.0.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
- '@radix-ui/react-slot': 1.2.0(@types/react@19.2.7)(react@19.2.1)
- react: 19.2.1
- react-dom: 19.2.1(react@19.2.1)
+ '@radix-ui/react-slot': 1.2.0(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
optionalDependencies:
- '@types/react': 19.2.7
- '@types/react-dom': 19.2.3(@types/react@19.2.7)
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
- '@radix-ui/react-slot@1.2.0(@types/react@19.2.7)(react@19.2.1)':
+ '@radix-ui/react-slot@1.2.0(@types/react@19.2.14)(react@19.2.4)':
dependencies:
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.1)
- react: 19.2.1
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
optionalDependencies:
- '@types/react': 19.2.7
+ '@types/react': 19.2.14
- '@radix-ui/react-switch@1.1.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)':
+ '@radix-ui/react-switch@1.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@radix-ui/primitive': 1.1.2
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.1)
- '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.1)
- '@radix-ui/react-primitive': 2.0.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
- '@radix-ui/react-use-controllable-state': 1.1.1(@types/react@19.2.7)(react@19.2.1)
- '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.7)(react@19.2.1)
- '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.7)(react@19.2.1)
- react: 19.2.1
- react-dom: 19.2.1(react@19.2.1)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-primitive': 2.0.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-use-controllable-state': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
optionalDependencies:
- '@types/react': 19.2.7
- '@types/react-dom': 19.2.3(@types/react@19.2.7)
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
- '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.7)(react@19.2.1)':
+ '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.14)(react@19.2.4)':
dependencies:
- react: 19.2.1
+ react: 19.2.4
optionalDependencies:
- '@types/react': 19.2.7
+ '@types/react': 19.2.14
- '@radix-ui/react-use-controllable-state@1.1.1(@types/react@19.2.7)(react@19.2.1)':
+ '@radix-ui/react-use-controllable-state@1.1.1(@types/react@19.2.14)(react@19.2.4)':
dependencies:
- '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.1)
- react: 19.2.1
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
optionalDependencies:
- '@types/react': 19.2.7
+ '@types/react': 19.2.14
- '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.7)(react@19.2.1)':
+ '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.2.4)':
dependencies:
- react: 19.2.1
+ react: 19.2.4
optionalDependencies:
- '@types/react': 19.2.7
+ '@types/react': 19.2.14
- '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.7)(react@19.2.1)':
+ '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.14)(react@19.2.4)':
dependencies:
- react: 19.2.1
+ react: 19.2.4
optionalDependencies:
- '@types/react': 19.2.7
+ '@types/react': 19.2.14
- '@radix-ui/react-use-size@1.1.1(@types/react@19.2.7)(react@19.2.1)':
+ '@radix-ui/react-use-size@1.1.1(@types/react@19.2.14)(react@19.2.4)':
dependencies:
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.1)
- react: 19.2.1
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
optionalDependencies:
- '@types/react': 19.2.7
+ '@types/react': 19.2.14
- '@rollup/rollup-linux-x64-gnu@4.53.3':
+ '@rollup/rollup-linux-x64-gnu@4.57.1':
optional: true
- '@rsbuild/core@1.6.12':
+ '@rsbuild/core@1.6.0-beta.1':
+ dependencies:
+ '@rspack/core': 1.6.0-beta.1(@swc/helpers@0.5.18)
+ '@rspack/lite-tapable': 1.0.1
+ '@swc/helpers': 0.5.18
+ core-js: 3.46.0
+ jiti: 2.6.1
+
+ '@rsbuild/core@1.7.3':
dependencies:
- '@rspack/core': 1.6.6(@swc/helpers@0.5.17)
+ '@rspack/core': 1.7.6(@swc/helpers@0.5.18)
'@rspack/lite-tapable': 1.1.0
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
core-js: 3.47.0
jiti: 2.6.1
- '@rsbuild/plugin-check-syntax@1.5.0(@rsbuild/core@1.6.12)':
+ '@rsbuild/plugin-check-syntax@1.6.1(@rsbuild/core@1.6.0-beta.1)':
+ dependencies:
+ acorn: 8.15.0
+ browserslist-to-es-version: 1.4.1
+ htmlparser2: 10.0.0
+ picocolors: 1.1.1
+ source-map: 0.7.6
+ optionalDependencies:
+ '@rsbuild/core': 1.6.0-beta.1
+
+ '@rsbuild/plugin-check-syntax@1.6.1(@rsbuild/core@1.7.3)':
dependencies:
acorn: 8.15.0
- browserslist-to-es-version: 1.2.0
+ browserslist-to-es-version: 1.4.1
htmlparser2: 10.0.0
picocolors: 1.1.1
source-map: 0.7.6
optionalDependencies:
- '@rsbuild/core': 1.6.12
+ '@rsbuild/core': 1.7.3
+
+ '@rsdoctor/client@1.5.2': {}
- '@rsdoctor/client@1.3.12': {}
+ '@rsdoctor/core@1.5.2(@rsbuild/core@1.6.0-beta.1)(@rspack/core@1.7.6(@swc/helpers@0.5.18))':
+ dependencies:
+ '@rsbuild/plugin-check-syntax': 1.6.1(@rsbuild/core@1.6.0-beta.1)
+ '@rsdoctor/graph': 1.5.2(@rspack/core@1.7.6(@swc/helpers@0.5.18))
+ '@rsdoctor/sdk': 1.5.2(@rspack/core@1.7.6(@swc/helpers@0.5.18))
+ '@rsdoctor/types': 1.5.2(@rspack/core@1.7.6(@swc/helpers@0.5.18))
+ '@rsdoctor/utils': 1.5.2(@rspack/core@1.7.6(@swc/helpers@0.5.18))
+ browserslist-load-config: 1.0.1
+ enhanced-resolve: 5.12.0
+ es-toolkit: 1.44.0
+ filesize: 10.1.6
+ fs-extra: 11.3.3
+ semver: 7.7.4
+ source-map: 0.7.6
+ transitivePeerDependencies:
+ - '@rsbuild/core'
+ - '@rspack/core'
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+ - webpack
- '@rsdoctor/core@1.3.12(@rsbuild/core@1.6.12)(@rspack/core@1.6.6(@swc/helpers@0.5.17))':
+ '@rsdoctor/core@1.5.2(@rsbuild/core@1.7.3)(@rspack/core@1.7.6(@swc/helpers@0.5.18))':
dependencies:
- '@rsbuild/plugin-check-syntax': 1.5.0(@rsbuild/core@1.6.12)
- '@rsdoctor/graph': 1.3.12(@rspack/core@1.6.6(@swc/helpers@0.5.17))
- '@rsdoctor/sdk': 1.3.12(@rspack/core@1.6.6(@swc/helpers@0.5.17))
- '@rsdoctor/types': 1.3.12(@rspack/core@1.6.6(@swc/helpers@0.5.17))
- '@rsdoctor/utils': 1.3.12(@rspack/core@1.6.6(@swc/helpers@0.5.17))
+ '@rsbuild/plugin-check-syntax': 1.6.1(@rsbuild/core@1.7.3)
+ '@rsdoctor/graph': 1.5.2(@rspack/core@1.7.6(@swc/helpers@0.5.18))
+ '@rsdoctor/sdk': 1.5.2(@rspack/core@1.7.6(@swc/helpers@0.5.18))
+ '@rsdoctor/types': 1.5.2(@rspack/core@1.7.6(@swc/helpers@0.5.18))
+ '@rsdoctor/utils': 1.5.2(@rspack/core@1.7.6(@swc/helpers@0.5.18))
browserslist-load-config: 1.0.1
enhanced-resolve: 5.12.0
- es-toolkit: 1.42.0
+ es-toolkit: 1.44.0
filesize: 10.1.6
- fs-extra: 11.3.2
- semver: 7.7.3
+ fs-extra: 11.3.3
+ semver: 7.7.4
source-map: 0.7.6
transitivePeerDependencies:
- '@rsbuild/core'
@@ -4744,26 +5496,42 @@ snapshots:
- utf-8-validate
- webpack
- '@rsdoctor/graph@1.3.12(@rspack/core@1.6.6(@swc/helpers@0.5.17))':
+ '@rsdoctor/graph@1.5.2(@rspack/core@1.7.6(@swc/helpers@0.5.18))':
dependencies:
- '@rsdoctor/types': 1.3.12(@rspack/core@1.6.6(@swc/helpers@0.5.17))
- '@rsdoctor/utils': 1.3.12(@rspack/core@1.6.6(@swc/helpers@0.5.17))
- es-toolkit: 1.42.0
+ '@rsdoctor/types': 1.5.2(@rspack/core@1.7.6(@swc/helpers@0.5.18))
+ '@rsdoctor/utils': 1.5.2(@rspack/core@1.7.6(@swc/helpers@0.5.18))
+ es-toolkit: 1.44.0
path-browserify: 1.0.1
source-map: 0.7.6
transitivePeerDependencies:
- '@rspack/core'
- webpack
- '@rsdoctor/rspack-plugin@1.3.12(@rsbuild/core@1.6.12)(@rspack/core@1.6.6(@swc/helpers@0.5.17))':
+ '@rsdoctor/rspack-plugin@1.5.2(@rsbuild/core@1.6.0-beta.1)(@rspack/core@1.7.6(@swc/helpers@0.5.18))':
+ dependencies:
+ '@rsdoctor/core': 1.5.2(@rsbuild/core@1.6.0-beta.1)(@rspack/core@1.7.6(@swc/helpers@0.5.18))
+ '@rsdoctor/graph': 1.5.2(@rspack/core@1.7.6(@swc/helpers@0.5.18))
+ '@rsdoctor/sdk': 1.5.2(@rspack/core@1.7.6(@swc/helpers@0.5.18))
+ '@rsdoctor/types': 1.5.2(@rspack/core@1.7.6(@swc/helpers@0.5.18))
+ '@rsdoctor/utils': 1.5.2(@rspack/core@1.7.6(@swc/helpers@0.5.18))
+ optionalDependencies:
+ '@rspack/core': 1.7.6(@swc/helpers@0.5.18)
+ transitivePeerDependencies:
+ - '@rsbuild/core'
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+ - webpack
+
+ '@rsdoctor/rspack-plugin@1.5.2(@rsbuild/core@1.7.3)(@rspack/core@1.7.6(@swc/helpers@0.5.18))':
dependencies:
- '@rsdoctor/core': 1.3.12(@rsbuild/core@1.6.12)(@rspack/core@1.6.6(@swc/helpers@0.5.17))
- '@rsdoctor/graph': 1.3.12(@rspack/core@1.6.6(@swc/helpers@0.5.17))
- '@rsdoctor/sdk': 1.3.12(@rspack/core@1.6.6(@swc/helpers@0.5.17))
- '@rsdoctor/types': 1.3.12(@rspack/core@1.6.6(@swc/helpers@0.5.17))
- '@rsdoctor/utils': 1.3.12(@rspack/core@1.6.6(@swc/helpers@0.5.17))
+ '@rsdoctor/core': 1.5.2(@rsbuild/core@1.7.3)(@rspack/core@1.7.6(@swc/helpers@0.5.18))
+ '@rsdoctor/graph': 1.5.2(@rspack/core@1.7.6(@swc/helpers@0.5.18))
+ '@rsdoctor/sdk': 1.5.2(@rspack/core@1.7.6(@swc/helpers@0.5.18))
+ '@rsdoctor/types': 1.5.2(@rspack/core@1.7.6(@swc/helpers@0.5.18))
+ '@rsdoctor/utils': 1.5.2(@rspack/core@1.7.6(@swc/helpers@0.5.18))
optionalDependencies:
- '@rspack/core': 1.6.6(@swc/helpers@0.5.17)
+ '@rspack/core': 1.7.6(@swc/helpers@0.5.18)
transitivePeerDependencies:
- '@rsbuild/core'
- bufferutil
@@ -4771,12 +5539,12 @@ snapshots:
- utf-8-validate
- webpack
- '@rsdoctor/sdk@1.3.12(@rspack/core@1.6.6(@swc/helpers@0.5.17))':
+ '@rsdoctor/sdk@1.5.2(@rspack/core@1.7.6(@swc/helpers@0.5.18))':
dependencies:
- '@rsdoctor/client': 1.3.12
- '@rsdoctor/graph': 1.3.12(@rspack/core@1.6.6(@swc/helpers@0.5.17))
- '@rsdoctor/types': 1.3.12(@rspack/core@1.6.6(@swc/helpers@0.5.17))
- '@rsdoctor/utils': 1.3.12(@rspack/core@1.6.6(@swc/helpers@0.5.17))
+ '@rsdoctor/client': 1.5.2
+ '@rsdoctor/graph': 1.5.2(@rspack/core@1.7.6(@swc/helpers@0.5.18))
+ '@rsdoctor/types': 1.5.2(@rspack/core@1.7.6(@swc/helpers@0.5.18))
+ '@rsdoctor/utils': 1.5.2(@rspack/core@1.7.6(@swc/helpers@0.5.18))
safer-buffer: 2.1.2
socket.io: 4.8.1
tapable: 2.2.3
@@ -4787,26 +5555,26 @@ snapshots:
- utf-8-validate
- webpack
- '@rsdoctor/types@1.3.12(@rspack/core@1.6.6(@swc/helpers@0.5.17))':
+ '@rsdoctor/types@1.5.2(@rspack/core@1.7.6(@swc/helpers@0.5.18))':
dependencies:
'@types/connect': 3.4.38
'@types/estree': 1.0.5
'@types/tapable': 2.2.7
source-map: 0.7.6
optionalDependencies:
- '@rspack/core': 1.6.6(@swc/helpers@0.5.17)
+ '@rspack/core': 1.7.6(@swc/helpers@0.5.18)
- '@rsdoctor/utils@1.3.12(@rspack/core@1.6.6(@swc/helpers@0.5.17))':
+ '@rsdoctor/utils@1.5.2(@rspack/core@1.7.6(@swc/helpers@0.5.18))':
dependencies:
'@babel/code-frame': 7.26.2
- '@rsdoctor/types': 1.3.12(@rspack/core@1.6.6(@swc/helpers@0.5.17))
+ '@rsdoctor/types': 1.5.2(@rspack/core@1.7.6(@swc/helpers@0.5.18))
'@types/estree': 1.0.5
acorn: 8.15.0
acorn-import-attributes: 1.9.5(acorn@8.15.0)
acorn-walk: 8.3.4
deep-eql: 4.1.4
- envinfo: 7.19.0
- fs-extra: 11.3.2
+ envinfo: 7.21.0
+ fs-extra: 11.3.3
get-port: 5.1.1
json-stream-stringify: 3.0.1
lines-and-columns: 2.0.4
@@ -4817,67 +5585,131 @@ snapshots:
- '@rspack/core'
- webpack
- '@rslib/core@0.18.3(typescript@5.9.3)':
+ '@rslib/core@0.16.1(typescript@5.9.3)':
+ dependencies:
+ '@rsbuild/core': 1.6.0-beta.1
+ rsbuild-plugin-dts: 0.16.1(@rsbuild/core@1.6.0-beta.1)(typescript@5.9.3)
+ optionalDependencies:
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - '@typescript/native-preview'
+
+ '@rslib/core@0.18.6(typescript@5.9.3)':
dependencies:
- '@rsbuild/core': 1.6.12
- rsbuild-plugin-dts: 0.18.3(@rsbuild/core@1.6.12)(typescript@5.9.3)
+ '@rsbuild/core': 1.7.3
+ rsbuild-plugin-dts: 0.18.6(@rsbuild/core@1.7.3)(typescript@5.9.3)
optionalDependencies:
typescript: 5.9.3
transitivePeerDependencies:
- '@typescript/native-preview'
- '@rspack/binding-darwin-arm64@1.6.6':
+ '@rspack/binding-darwin-arm64@1.6.0-beta.1':
+ optional: true
+
+ '@rspack/binding-darwin-arm64@1.7.6':
+ optional: true
+
+ '@rspack/binding-darwin-x64@1.6.0-beta.1':
+ optional: true
+
+ '@rspack/binding-darwin-x64@1.7.6':
+ optional: true
+
+ '@rspack/binding-linux-arm64-gnu@1.6.0-beta.1':
+ optional: true
+
+ '@rspack/binding-linux-arm64-gnu@1.7.6':
+ optional: true
+
+ '@rspack/binding-linux-arm64-musl@1.6.0-beta.1':
+ optional: true
+
+ '@rspack/binding-linux-arm64-musl@1.7.6':
optional: true
- '@rspack/binding-darwin-x64@1.6.6':
+ '@rspack/binding-linux-x64-gnu@1.6.0-beta.1':
optional: true
- '@rspack/binding-linux-arm64-gnu@1.6.6':
+ '@rspack/binding-linux-x64-gnu@1.7.6':
optional: true
- '@rspack/binding-linux-arm64-musl@1.6.6':
+ '@rspack/binding-linux-x64-musl@1.6.0-beta.1':
optional: true
- '@rspack/binding-linux-x64-gnu@1.6.6':
+ '@rspack/binding-linux-x64-musl@1.7.6':
optional: true
- '@rspack/binding-linux-x64-musl@1.6.6':
+ '@rspack/binding-wasm32-wasi@1.6.0-beta.1':
+ dependencies:
+ '@napi-rs/wasm-runtime': 1.0.7
optional: true
- '@rspack/binding-wasm32-wasi@1.6.6':
+ '@rspack/binding-wasm32-wasi@1.7.6':
dependencies:
'@napi-rs/wasm-runtime': 1.0.7
optional: true
- '@rspack/binding-win32-arm64-msvc@1.6.6':
+ '@rspack/binding-win32-arm64-msvc@1.6.0-beta.1':
+ optional: true
+
+ '@rspack/binding-win32-arm64-msvc@1.7.6':
+ optional: true
+
+ '@rspack/binding-win32-ia32-msvc@1.6.0-beta.1':
+ optional: true
+
+ '@rspack/binding-win32-ia32-msvc@1.7.6':
optional: true
- '@rspack/binding-win32-ia32-msvc@1.6.6':
+ '@rspack/binding-win32-x64-msvc@1.6.0-beta.1':
optional: true
- '@rspack/binding-win32-x64-msvc@1.6.6':
+ '@rspack/binding-win32-x64-msvc@1.7.6':
optional: true
- '@rspack/binding@1.6.6':
+ '@rspack/binding@1.6.0-beta.1':
+ optionalDependencies:
+ '@rspack/binding-darwin-arm64': 1.6.0-beta.1
+ '@rspack/binding-darwin-x64': 1.6.0-beta.1
+ '@rspack/binding-linux-arm64-gnu': 1.6.0-beta.1
+ '@rspack/binding-linux-arm64-musl': 1.6.0-beta.1
+ '@rspack/binding-linux-x64-gnu': 1.6.0-beta.1
+ '@rspack/binding-linux-x64-musl': 1.6.0-beta.1
+ '@rspack/binding-wasm32-wasi': 1.6.0-beta.1
+ '@rspack/binding-win32-arm64-msvc': 1.6.0-beta.1
+ '@rspack/binding-win32-ia32-msvc': 1.6.0-beta.1
+ '@rspack/binding-win32-x64-msvc': 1.6.0-beta.1
+
+ '@rspack/binding@1.7.6':
optionalDependencies:
- '@rspack/binding-darwin-arm64': 1.6.6
- '@rspack/binding-darwin-x64': 1.6.6
- '@rspack/binding-linux-arm64-gnu': 1.6.6
- '@rspack/binding-linux-arm64-musl': 1.6.6
- '@rspack/binding-linux-x64-gnu': 1.6.6
- '@rspack/binding-linux-x64-musl': 1.6.6
- '@rspack/binding-wasm32-wasi': 1.6.6
- '@rspack/binding-win32-arm64-msvc': 1.6.6
- '@rspack/binding-win32-ia32-msvc': 1.6.6
- '@rspack/binding-win32-x64-msvc': 1.6.6
-
- '@rspack/core@1.6.6(@swc/helpers@0.5.17)':
- dependencies:
- '@module-federation/runtime-tools': 0.21.6
- '@rspack/binding': 1.6.6
+ '@rspack/binding-darwin-arm64': 1.7.6
+ '@rspack/binding-darwin-x64': 1.7.6
+ '@rspack/binding-linux-arm64-gnu': 1.7.6
+ '@rspack/binding-linux-arm64-musl': 1.7.6
+ '@rspack/binding-linux-x64-gnu': 1.7.6
+ '@rspack/binding-linux-x64-musl': 1.7.6
+ '@rspack/binding-wasm32-wasi': 1.7.6
+ '@rspack/binding-win32-arm64-msvc': 1.7.6
+ '@rspack/binding-win32-ia32-msvc': 1.7.6
+ '@rspack/binding-win32-x64-msvc': 1.7.6
+
+ '@rspack/core@1.6.0-beta.1(@swc/helpers@0.5.18)':
+ dependencies:
+ '@module-federation/runtime-tools': 0.21.1
+ '@rspack/binding': 1.6.0-beta.1
+ '@rspack/lite-tapable': 1.0.1
+ optionalDependencies:
+ '@swc/helpers': 0.5.18
+
+ '@rspack/core@1.7.6(@swc/helpers@0.5.18)':
+ dependencies:
+ '@module-federation/runtime-tools': 0.22.0
+ '@rspack/binding': 1.7.6
'@rspack/lite-tapable': 1.1.0
optionalDependencies:
- '@swc/helpers': 0.5.17
+ '@swc/helpers': 0.5.18
+
+ '@rspack/lite-tapable@1.0.1': {}
'@rspack/lite-tapable@1.1.0': {}
@@ -4885,17 +5717,17 @@ snapshots:
'@sqltools/formatter@1.2.5': {}
- '@standard-schema/spec@1.0.0': {}
+ '@standard-schema/spec@1.1.0': {}
'@swc/helpers@0.5.15':
dependencies:
tslib: 2.8.1
- '@swc/helpers@0.5.17':
+ '@swc/helpers@0.5.18':
dependencies:
tslib: 2.8.1
- '@trpc/server@11.7.2(typescript@5.9.3)':
+ '@trpc/server@11.10.0(typescript@5.9.3)':
dependencies:
typescript: 5.9.3
@@ -4906,28 +5738,34 @@ snapshots:
'@types/better-sqlite3@7.6.13':
dependencies:
- '@types/node': 24.10.1
+ '@types/node': 24.10.13
optional: true
'@types/connect@3.4.38':
dependencies:
- '@types/node': 24.10.1
+ '@types/node': 24.10.13
'@types/cors@2.8.19':
dependencies:
- '@types/node': 24.10.1
+ '@types/node': 24.10.13
'@types/estree@1.0.5': {}
- '@types/node@24.10.1':
+ '@types/figlet@1.7.0': {}
+
+ '@types/node@24.10.13':
+ dependencies:
+ undici-types: 7.16.0
+
+ '@types/node@25.2.3':
dependencies:
undici-types: 7.16.0
- '@types/react-dom@19.2.3(@types/react@19.2.7)':
+ '@types/react-dom@19.2.3(@types/react@19.2.14)':
dependencies:
- '@types/react': 19.2.7
+ '@types/react': 19.2.14
- '@types/react@19.2.7':
+ '@types/react@19.2.14':
dependencies:
csstype: 3.2.3
@@ -4937,7 +5775,7 @@ snapshots:
'@types/ws@8.18.1':
dependencies:
- '@types/node': 24.10.1
+ '@types/node': 24.10.13
optional: true
accepts@1.3.8:
@@ -4965,7 +5803,7 @@ snapshots:
ansi-styles@6.2.3: {}
- ansis@4.2.0: {}
+ ansis@3.17.0: {}
app-root-path@3.1.0: {}
@@ -4981,9 +5819,9 @@ snapshots:
base64id@2.0.0: {}
- baseline-browser-mapping@2.9.2: {}
+ baseline-browser-mapping@2.9.19: {}
- better-sqlite3@12.5.0:
+ better-sqlite3@12.4.1:
dependencies:
bindings: 1.5.0
prebuild-install: 7.1.3
@@ -5007,17 +5845,17 @@ snapshots:
browserslist-load-config@1.0.1: {}
- browserslist-to-es-version@1.2.0:
+ browserslist-to-es-version@1.4.1:
dependencies:
browserslist: 4.28.1
browserslist@4.28.1:
dependencies:
- baseline-browser-mapping: 2.9.2
- caniuse-lite: 1.0.30001759
- electron-to-chromium: 1.5.266
+ baseline-browser-mapping: 2.9.19
+ caniuse-lite: 1.0.30001770
+ electron-to-chromium: 1.5.286
node-releases: 2.0.27
- update-browserslist-db: 1.2.2(browserslist@4.28.1)
+ update-browserslist-db: 1.2.3(browserslist@4.28.1)
buffer-from@1.1.2: {}
@@ -5032,13 +5870,61 @@ snapshots:
base64-js: 1.5.1
ieee754: 1.2.1
- c15t@1.8.1(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(@opentelemetry/instrumentation@0.208.0(@opentelemetry/api@1.9.0))(@types/better-sqlite3@7.6.13)(@types/react@19.2.7)(better-sqlite3@12.5.0)(react@19.2.1)(typeorm@0.3.28(better-sqlite3@12.5.0))(use-sync-external-store@1.6.0(react@19.2.1))(ws@8.18.3):
+ c15t@1.7.1(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(@opentelemetry/instrumentation@0.207.0(@opentelemetry/api@1.9.0))(@types/better-sqlite3@7.6.13)(@types/react@19.2.14)(better-sqlite3@12.4.1)(react@19.2.4)(typeorm@0.3.27(better-sqlite3@12.4.1)(reflect-metadata@0.2.2))(use-sync-external-store@1.6.0(react@19.2.4))(ws@8.18.3):
+ dependencies:
+ '@c15t/backend': 1.7.1(@libsql/client@0.15.15)(@opentelemetry/instrumentation@0.207.0(@opentelemetry/api@1.9.0))(@types/better-sqlite3@7.6.13)(better-sqlite3@12.4.1)(typeorm@0.3.27(better-sqlite3@12.4.1)(reflect-metadata@0.2.2))(ws@8.18.3)
+ '@c15t/translations': 1.7.0
+ '@orpc/client': 1.8.1(@opentelemetry/api@1.9.0)
+ '@orpc/server': 1.8.1(@opentelemetry/api@1.9.0)(ws@8.18.3)
+ zustand: 5.0.11(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))
+ transitivePeerDependencies:
+ - '@aws-sdk/client-rds-data'
+ - '@cloudflare/workers-types'
+ - '@electric-sql/pglite'
+ - '@libsql/client'
+ - '@libsql/client-wasm'
+ - '@neondatabase/serverless'
+ - '@op-engineering/op-sqlite'
+ - '@opentelemetry/api'
+ - '@opentelemetry/instrumentation'
+ - '@planetscale/database'
+ - '@prisma/client'
+ - '@tidbcloud/serverless'
+ - '@types/better-sqlite3'
+ - '@types/pg'
+ - '@types/react'
+ - '@types/sql.js'
+ - '@upstash/redis'
+ - '@vercel/postgres'
+ - '@xata.io/client'
+ - better-sqlite3
+ - bun-types
+ - convex
+ - crossws
+ - expo-sqlite
+ - gel
+ - immer
+ - knex
+ - mongodb
+ - mysql2
+ - pg
+ - postgres
+ - prisma
+ - react
+ - sql.js
+ - sqlite3
+ - supports-color
+ - typeorm
+ - use-sync-external-store
+ - ws
+
+ c15t@1.8.1(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(@opentelemetry/instrumentation@0.207.0(@opentelemetry/api@1.9.0))(@types/better-sqlite3@7.6.13)(@types/react@19.2.14)(better-sqlite3@12.4.1)(react@19.2.4)(typeorm@0.3.27(better-sqlite3@12.4.1)(reflect-metadata@0.2.2))(use-sync-external-store@1.6.0(react@19.2.4))(ws@8.18.3):
dependencies:
- '@c15t/backend': 1.8.0(@libsql/client@0.15.15)(@opentelemetry/instrumentation@0.208.0(@opentelemetry/api@1.9.0))(@types/better-sqlite3@7.6.13)(better-sqlite3@12.5.0)(typeorm@0.3.28(better-sqlite3@12.5.0))(ws@8.18.3)
+ '@c15t/backend': 1.8.0(@libsql/client@0.15.15)(@opentelemetry/instrumentation@0.207.0(@opentelemetry/api@1.9.0))(@types/better-sqlite3@7.6.13)(better-sqlite3@12.4.1)(typeorm@0.3.27(better-sqlite3@12.4.1)(reflect-metadata@0.2.2))(ws@8.18.3)
'@c15t/translations': 1.8.0
'@orpc/client': 1.8.1(@opentelemetry/api@1.9.0)
'@orpc/server': 1.8.1(@opentelemetry/api@1.9.0)(ws@8.18.3)
- zustand: 5.0.9(@types/react@19.2.7)(react@19.2.1)(use-sync-external-store@1.6.0(react@19.2.1))
+ zustand: 5.0.11(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))
transitivePeerDependencies:
- '@aws-sdk/client-rds-data'
- '@cloudflare/workers-types'
@@ -5097,19 +5983,19 @@ snapshots:
call-bind-apply-helpers: 1.0.2
get-intrinsic: 1.3.0
- caniuse-lite@1.0.30001759: {}
+ caniuse-lite@1.0.30001770: {}
chalk@5.6.2: {}
chownr@1.1.4:
optional: true
- citty@0.1.6:
- dependencies:
- consola: 3.4.2
+ citty@0.2.1: {}
cjs-module-lexer@1.4.3: {}
+ cjs-module-lexer@2.2.0: {}
+
cli-table3@0.6.5:
dependencies:
string-width: 4.2.3
@@ -5132,11 +6018,7 @@ snapshots:
color-name@1.1.4: {}
- commander@14.0.2: {}
-
- confbox@0.2.2: {}
-
- consola@3.4.2: {}
+ commander@14.0.3: {}
cookie@0.7.2: {}
@@ -5146,9 +6028,11 @@ snapshots:
dependencies:
is-what: 5.5.0
+ core-js@3.46.0: {}
+
core-js@3.47.0: {}
- cors@2.8.5:
+ cors@2.8.6:
dependencies:
object-assign: 4.1.1
vary: 1.1.2
@@ -5179,7 +6063,7 @@ snapshots:
mimic-response: 3.1.0
optional: true
- dedent@1.7.0: {}
+ dedent@1.7.1: {}
deep-eql@4.1.4:
dependencies:
@@ -5224,9 +6108,9 @@ snapshots:
dotenv@16.6.1: {}
- dotenv@17.2.3: {}
+ dotenv@17.3.1: {}
- drizzle-kit@0.31.8:
+ drizzle-kit@0.31.9:
dependencies:
'@drizzle-team/brocli': 0.10.2
'@esbuild-kit/esm-loader': 2.6.5
@@ -5235,12 +6119,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
- drizzle-orm@0.44.7(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.5.0)(kysely@0.27.6):
+ drizzle-orm@0.44.7(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.4.1)(kysely@0.27.6):
optionalDependencies:
'@libsql/client': 0.15.15
'@opentelemetry/api': 1.9.0
'@types/better-sqlite3': 7.6.13
- better-sqlite3: 12.5.0
+ better-sqlite3: 12.4.1
kysely: 0.27.6
dunder-proto@1.0.1:
@@ -5251,7 +6135,7 @@ snapshots:
eastasianwidth@0.2.0: {}
- electron-to-chromium@1.5.266: {}
+ electron-to-chromium@1.5.286: {}
emoji-regex@8.0.0: {}
@@ -5264,17 +6148,17 @@ snapshots:
engine.io-parser@5.2.3: {}
- engine.io@6.6.4:
+ engine.io@6.6.5:
dependencies:
'@types/cors': 2.8.19
- '@types/node': 24.10.1
+ '@types/node': 24.10.13
accepts: 1.3.8
base64id: 2.0.0
cookie: 0.7.2
- cors: 2.8.5
- debug: 4.3.7
+ cors: 2.8.6
+ debug: 4.4.3
engine.io-parser: 5.2.3
- ws: 8.17.1
+ ws: 8.18.3
transitivePeerDependencies:
- bufferutil
- supports-color
@@ -5289,7 +6173,7 @@ snapshots:
entities@6.0.1: {}
- envinfo@7.19.0: {}
+ envinfo@7.21.0: {}
es-define-property@1.0.1: {}
@@ -5299,7 +6183,7 @@ snapshots:
dependencies:
es-errors: 1.3.0
- es-toolkit@1.42.0: {}
+ es-toolkit@1.44.0: {}
esbuild-register@3.6.0(esbuild@0.25.12):
dependencies:
@@ -5369,14 +6253,16 @@ snapshots:
expand-template@2.0.3:
optional: true
- exsolve@1.0.8: {}
-
fetch-blob@3.2.0:
dependencies:
node-domexception: 1.0.0
web-streams-polyfill: 3.3.3
optional: true
+ figlet@1.10.0:
+ dependencies:
+ commander: 14.0.3
+
file-uri-to-path@1.0.0:
optional: true
@@ -5399,7 +6285,7 @@ snapshots:
fs-constants@1.0.0:
optional: true
- fs-extra@11.3.2:
+ fs-extra@11.3.3:
dependencies:
graceful-fs: 4.2.11
jsonfile: 6.2.0
@@ -5408,18 +6294,18 @@ snapshots:
fsevents@2.3.2:
optional: true
- fumadb@0.1.2(drizzle-orm@0.44.7(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.5.0)(kysely@0.27.6))(typeorm@0.3.28(better-sqlite3@12.5.0)):
+ fumadb@0.1.2(drizzle-orm@0.44.7(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.4.1)(kysely@0.27.6))(typeorm@0.3.27(better-sqlite3@12.4.1)(reflect-metadata@0.2.2)):
dependencies:
'@clack/prompts': 0.11.0
'@paralleldrive/cuid2': 2.3.1
- commander: 14.0.2
- kysely: 0.28.8
- kysely-typeorm: 0.3.0(kysely@0.28.8)(typeorm@0.3.28(better-sqlite3@12.5.0))
- semver: 7.7.3
- zod: 4.1.13
+ commander: 14.0.3
+ kysely: 0.28.11
+ kysely-typeorm: 0.3.0(kysely@0.28.11)(typeorm@0.3.27(better-sqlite3@12.4.1)(reflect-metadata@0.2.2))
+ semver: 7.7.4
+ zod: 4.3.6
optionalDependencies:
- drizzle-orm: 0.44.7(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.5.0)(kysely@0.27.6)
- typeorm: 0.3.28(better-sqlite3@12.5.0)
+ drizzle-orm: 0.44.7(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.4.1)(kysely@0.27.6)
+ typeorm: 0.3.27(better-sqlite3@12.4.1)(reflect-metadata@0.2.2)
function-bind@1.1.2: {}
@@ -5445,7 +6331,7 @@ snapshots:
dunder-proto: 1.0.1
es-object-atoms: 1.1.1
- get-tsconfig@4.13.0:
+ get-tsconfig@4.13.6:
dependencies:
resolve-pkg-maps: 1.0.0
@@ -5461,12 +6347,6 @@ snapshots:
package-json-from-dist: 1.0.1
path-scurry: 1.11.1
- glob@13.0.0:
- dependencies:
- minimatch: 10.1.1
- minipass: 7.1.2
- path-scurry: 2.0.1
-
gopd@1.2.0: {}
graceful-fs@4.2.11: {}
@@ -5501,11 +6381,11 @@ snapshots:
cjs-module-lexer: 1.4.3
module-details-from-path: 1.0.4
- import-in-the-middle@2.0.0:
+ import-in-the-middle@2.0.6:
dependencies:
acorn: 8.15.0
acorn-import-attributes: 1.9.5(acorn@8.15.0)
- cjs-module-lexer: 1.4.3
+ cjs-module-lexer: 2.2.0
module-details-from-path: 1.0.4
inherits@2.0.4: {}
@@ -5523,7 +6403,7 @@ snapshots:
is-typed-array@1.1.15:
dependencies:
- which-typed-array: 1.1.19
+ which-typed-array: 1.1.20
is-what@5.5.0: {}
@@ -5554,14 +6434,14 @@ snapshots:
optionalDependencies:
graceful-fs: 4.2.11
- kysely-typeorm@0.3.0(kysely@0.28.8)(typeorm@0.3.28(better-sqlite3@12.5.0)):
+ kysely-typeorm@0.3.0(kysely@0.28.11)(typeorm@0.3.27(better-sqlite3@12.4.1)(reflect-metadata@0.2.2)):
dependencies:
- kysely: 0.28.8
- typeorm: 0.3.28(better-sqlite3@12.5.0)
+ kysely: 0.28.11
+ typeorm: 0.3.27(better-sqlite3@12.4.1)(reflect-metadata@0.2.2)
kysely@0.27.6: {}
- kysely@0.28.8: {}
+ kysely@0.28.11: {}
libsql@0.5.22:
dependencies:
@@ -5591,8 +6471,6 @@ snapshots:
lru-cache@10.4.3: {}
- lru-cache@11.2.4: {}
-
math-intrinsics@1.1.0: {}
mime-db@1.52.0: {}
@@ -5604,10 +6482,6 @@ snapshots:
mimic-response@3.1.0:
optional: true
- minimatch@10.1.1:
- dependencies:
- '@isaacs/brace-expansion': 5.0.0
-
minimatch@9.0.5:
dependencies:
brace-expansion: 2.0.2
@@ -5633,36 +6507,37 @@ snapshots:
neverthrow@8.2.0:
optionalDependencies:
- '@rollup/rollup-linux-x64-gnu': 4.53.3
+ '@rollup/rollup-linux-x64-gnu': 4.57.1
- next@16.0.7(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.1(react@19.2.1))(react@19.2.1):
+ next@16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
- '@next/env': 16.0.7
+ '@next/env': 16.1.6
'@swc/helpers': 0.5.15
- caniuse-lite: 1.0.30001759
+ baseline-browser-mapping: 2.9.19
+ caniuse-lite: 1.0.30001770
postcss: 8.4.31
- react: 19.2.1
- react-dom: 19.2.1(react@19.2.1)
- styled-jsx: 5.1.6(react@19.2.1)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ styled-jsx: 5.1.6(react@19.2.4)
optionalDependencies:
- '@next/swc-darwin-arm64': 16.0.7
- '@next/swc-darwin-x64': 16.0.7
- '@next/swc-linux-arm64-gnu': 16.0.7
- '@next/swc-linux-arm64-musl': 16.0.7
- '@next/swc-linux-x64-gnu': 16.0.7
- '@next/swc-linux-x64-musl': 16.0.7
- '@next/swc-win32-arm64-msvc': 16.0.7
- '@next/swc-win32-x64-msvc': 16.0.7
+ '@next/swc-darwin-arm64': 16.1.6
+ '@next/swc-darwin-x64': 16.1.6
+ '@next/swc-linux-arm64-gnu': 16.1.6
+ '@next/swc-linux-arm64-musl': 16.1.6
+ '@next/swc-linux-x64-gnu': 16.1.6
+ '@next/swc-linux-x64-musl': 16.1.6
+ '@next/swc-win32-arm64-msvc': 16.1.6
+ '@next/swc-win32-x64-msvc': 16.1.6
'@opentelemetry/api': 1.9.0
- '@playwright/test': 1.57.0
+ '@playwright/test': 1.58.2
sharp: 0.34.5
transitivePeerDependencies:
- '@babel/core'
- babel-plugin-macros
- node-abi@3.85.0:
+ node-abi@3.87.0:
dependencies:
- semver: 7.7.3
+ semver: 7.7.4
optional: true
node-domexception@1.0.0:
@@ -5677,12 +6552,10 @@ snapshots:
node-releases@2.0.27: {}
- nypm@0.6.2:
+ nypm@0.6.5:
dependencies:
- citty: 0.1.6
- consola: 3.4.2
+ citty: 0.2.1
pathe: 2.0.3
- pkg-types: 2.3.0
tinyexec: 1.0.2
object-assign@4.1.1: {}
@@ -5694,10 +6567,6 @@ snapshots:
openapi-types@12.1.3: {}
- p-limit@7.2.0:
- dependencies:
- yocto-queue: 1.2.2
-
package-json-from-dist@1.0.1: {}
package-manager-detector@1.6.0: {}
@@ -5715,30 +6584,23 @@ snapshots:
lru-cache: 10.4.3
minipass: 7.1.2
- path-scurry@2.0.1:
- dependencies:
- lru-cache: 11.2.4
- minipass: 7.1.2
-
pathe@2.0.3: {}
- picocolors@1.1.1: {}
-
- pkg-types@2.3.0:
+ perfume.js@9.4.0:
dependencies:
- confbox: 0.2.2
- exsolve: 1.0.8
- pathe: 2.0.3
+ web-vitals: 3.5.2
+
+ picocolors@1.1.1: {}
- playwright-core@1.57.0: {}
+ playwright-core@1.58.2: {}
- playwright-performance-metrics@1.2.4(@playwright/test@1.57.0):
+ playwright-performance-metrics@1.2.5(@playwright/test@1.58.2):
dependencies:
- '@playwright/test': 1.57.0
+ '@playwright/test': 1.58.2
- playwright@1.57.0:
+ playwright@1.58.2:
dependencies:
- playwright-core: 1.57.0
+ playwright-core: 1.58.2
optionalDependencies:
fsevents: 2.3.2
@@ -5758,7 +6620,7 @@ snapshots:
minimist: 1.2.8
mkdirp-classic: 0.5.3
napi-build-utils: 2.0.0
- node-abi: 3.85.0
+ node-abi: 3.87.0
pump: 3.0.3
rc: 1.2.8
simple-get: 4.0.1
@@ -5791,7 +6653,7 @@ snapshots:
'@protobufjs/path': 1.1.2
'@protobufjs/pool': 1.1.0
'@protobufjs/utf8': 1.1.0
- '@types/node': 24.10.1
+ '@types/node': 24.10.13
long: 5.3.2
pump@3.0.3:
@@ -5810,14 +6672,14 @@ snapshots:
strip-json-comments: 2.0.1
optional: true
- react-dom@19.2.1(react@19.2.1):
+ react-dom@19.2.4(react@19.2.4):
dependencies:
- react: 19.2.1
+ react: 19.2.4
scheduler: 0.27.0
react-is@16.13.1: {}
- react@19.2.1: {}
+ react@19.2.4: {}
readable-stream@3.6.2:
dependencies:
@@ -5853,12 +6715,19 @@ snapshots:
path-parse: 1.0.7
supports-preserve-symlinks-flag: 1.0.0
- rou3@0.7.10: {}
+ rou3@0.7.12: {}
- rsbuild-plugin-dts@0.18.3(@rsbuild/core@1.6.12)(typescript@5.9.3):
+ rsbuild-plugin-dts@0.16.1(@rsbuild/core@1.6.0-beta.1)(typescript@5.9.3):
dependencies:
'@ast-grep/napi': 0.37.0
- '@rsbuild/core': 1.6.12
+ '@rsbuild/core': 1.6.0-beta.1
+ optionalDependencies:
+ typescript: 5.9.3
+
+ rsbuild-plugin-dts@0.18.6(@rsbuild/core@1.7.3)(typescript@5.9.3):
+ dependencies:
+ '@ast-grep/napi': 0.37.0
+ '@rsbuild/core': 1.7.3
optionalDependencies:
typescript: 5.9.3
@@ -5870,7 +6739,7 @@ snapshots:
scheduler@0.27.0: {}
- semver@7.7.3: {}
+ semver@7.7.4: {}
set-function-length@1.2.2:
dependencies:
@@ -5891,7 +6760,7 @@ snapshots:
dependencies:
'@img/colour': 1.0.0
detect-libc: 2.1.2
- semver: 7.7.3
+ semver: 7.7.4
optionalDependencies:
'@img/sharp-darwin-arm64': 0.34.5
'@img/sharp-darwin-x64': 0.34.5
@@ -5939,19 +6808,19 @@ snapshots:
sisteransi@1.0.5: {}
- socket.io-adapter@2.5.5:
+ socket.io-adapter@2.5.6:
dependencies:
- debug: 4.3.7
- ws: 8.17.1
+ debug: 4.4.3
+ ws: 8.18.3
transitivePeerDependencies:
- bufferutil
- supports-color
- utf-8-validate
- socket.io-parser@4.2.4:
+ socket.io-parser@4.2.5:
dependencies:
'@socket.io/component-emitter': 3.1.2
- debug: 4.3.7
+ debug: 4.4.3
transitivePeerDependencies:
- supports-color
@@ -5959,11 +6828,11 @@ snapshots:
dependencies:
accepts: 1.3.8
base64id: 2.0.0
- cors: 2.8.5
+ cors: 2.8.6
debug: 4.3.7
- engine.io: 6.6.4
- socket.io-adapter: 2.5.5
- socket.io-parser: 4.2.4
+ engine.io: 6.6.5
+ socket.io-adapter: 2.5.6
+ socket.io-parser: 4.2.5
transitivePeerDependencies:
- bufferutil
- supports-color
@@ -6010,10 +6879,10 @@ snapshots:
strip-json-comments@2.0.1:
optional: true
- styled-jsx@5.1.6(react@19.2.1):
+ styled-jsx@5.1.6(react@19.2.4):
dependencies:
client-only: 0.0.1
- react: 19.2.1
+ react: 19.2.4
superjson@2.2.6:
dependencies:
@@ -6052,13 +6921,13 @@ snapshots:
safe-buffer: 5.2.1
typed-array-buffer: 1.0.3
- trpc-cli@0.12.1(@orpc/server@1.12.2(@opentelemetry/api@1.9.0)(ws@8.18.3))(@trpc/server@11.7.2(typescript@5.9.3))(zod@4.1.13):
+ trpc-cli@0.12.2(@orpc/server@1.8.1(@opentelemetry/api@1.9.0)(ws@8.18.3))(@trpc/server@11.10.0(typescript@5.9.3))(zod@4.3.6):
dependencies:
- commander: 14.0.2
+ commander: 14.0.3
optionalDependencies:
- '@orpc/server': 1.12.2(@opentelemetry/api@1.9.0)(ws@8.18.3)
- '@trpc/server': 11.7.2(typescript@5.9.3)
- zod: 4.1.13
+ '@orpc/server': 1.8.1(@opentelemetry/api@1.9.0)(ws@8.18.3)
+ '@trpc/server': 11.10.0(typescript@5.9.3)
+ zod: 4.3.6
tslib@2.8.1: {}
@@ -6067,38 +6936,38 @@ snapshots:
safe-buffer: 5.2.1
optional: true
- turbo-darwin-64@2.6.3:
+ turbo-darwin-64@2.8.9:
optional: true
- turbo-darwin-arm64@2.6.3:
+ turbo-darwin-arm64@2.8.9:
optional: true
- turbo-linux-64@2.6.3:
+ turbo-linux-64@2.8.9:
optional: true
- turbo-linux-arm64@2.6.3:
+ turbo-linux-arm64@2.8.9:
optional: true
- turbo-windows-64@2.6.3:
+ turbo-windows-64@2.8.9:
optional: true
- turbo-windows-arm64@2.6.3:
+ turbo-windows-arm64@2.8.9:
optional: true
- turbo@2.6.3:
+ turbo@2.8.9:
optionalDependencies:
- turbo-darwin-64: 2.6.3
- turbo-darwin-arm64: 2.6.3
- turbo-linux-64: 2.6.3
- turbo-linux-arm64: 2.6.3
- turbo-windows-64: 2.6.3
- turbo-windows-arm64: 2.6.3
+ turbo-darwin-64: 2.8.9
+ turbo-darwin-arm64: 2.8.9
+ turbo-linux-64: 2.8.9
+ turbo-linux-arm64: 2.8.9
+ turbo-windows-64: 2.8.9
+ turbo-windows-arm64: 2.8.9
type-detect@4.1.0: {}
type-fest@4.41.0: {}
- type-fest@5.3.0:
+ type-fest@5.4.4:
dependencies:
tagged-tag: 1.0.0
@@ -6108,15 +6977,15 @@ snapshots:
es-errors: 1.3.0
is-typed-array: 1.1.15
- typeorm@0.3.28(better-sqlite3@12.5.0):
+ typeorm@0.3.27(better-sqlite3@12.4.1)(reflect-metadata@0.2.2):
dependencies:
'@sqltools/formatter': 1.2.5
- ansis: 4.2.0
+ ansis: 3.17.0
app-root-path: 3.1.0
buffer: 6.0.3
dayjs: 1.11.19
debug: 4.4.3
- dedent: 1.7.0
+ dedent: 1.7.1
dotenv: 16.6.1
glob: 10.5.0
reflect-metadata: 0.2.2
@@ -6126,23 +6995,22 @@ snapshots:
uuid: 11.1.0
yargs: 17.7.2
optionalDependencies:
- better-sqlite3: 12.5.0
+ better-sqlite3: 12.4.1
transitivePeerDependencies:
- babel-plugin-macros
- supports-color
typescript@5.9.3: {}
- ultracite@6.3.9(@orpc/server@1.12.2(@opentelemetry/api@1.9.0)(ws@8.18.3))(typescript@5.9.3):
+ ultracite@6.0.5(@orpc/server@1.8.1(@opentelemetry/api@1.9.0)(ws@8.18.3))(typescript@5.9.3):
dependencies:
'@clack/prompts': 0.11.0
- '@trpc/server': 11.7.2(typescript@5.9.3)
+ '@trpc/server': 11.10.0(typescript@5.9.3)
deepmerge: 4.3.1
- glob: 13.0.0
jsonc-parser: 3.3.1
- nypm: 0.6.2
- trpc-cli: 0.12.1(@orpc/server@1.12.2(@opentelemetry/api@1.9.0)(ws@8.18.3))(@trpc/server@11.7.2(typescript@5.9.3))(zod@4.1.13)
- zod: 4.1.13
+ nypm: 0.6.5
+ trpc-cli: 0.12.2(@orpc/server@1.8.1(@opentelemetry/api@1.9.0)(ws@8.18.3))(@trpc/server@11.10.0(typescript@5.9.3))(zod@4.3.6)
+ zod: 4.3.6
transitivePeerDependencies:
- '@orpc/server'
- '@valibot/to-json-schema'
@@ -6154,15 +7022,15 @@ snapshots:
universalify@2.0.1: {}
- update-browserslist-db@1.2.2(browserslist@4.28.1):
+ update-browserslist-db@1.2.3(browserslist@4.28.1):
dependencies:
browserslist: 4.28.1
escalade: 3.2.0
picocolors: 1.1.1
- use-sync-external-store@1.6.0(react@19.2.1):
+ use-sync-external-store@1.6.0(react@19.2.4):
dependencies:
- react: 19.2.1
+ react: 19.2.4
optional: true
util-deprecate@1.0.2:
@@ -6175,7 +7043,9 @@ snapshots:
web-streams-polyfill@3.3.3:
optional: true
- which-typed-array@1.1.19:
+ web-vitals@3.5.2: {}
+
+ which-typed-array@1.1.20:
dependencies:
available-typed-arrays: 1.0.7
call-bind: 1.0.8
@@ -6206,10 +7076,7 @@ snapshots:
wrappy@1.0.2:
optional: true
- ws@8.17.1: {}
-
- ws@8.18.3:
- optional: true
+ ws@8.18.3: {}
y18n@5.0.8: {}
@@ -6225,12 +7092,10 @@ snapshots:
y18n: 5.0.8
yargs-parser: 21.1.1
- yocto-queue@1.2.2: {}
-
- zod@4.1.13: {}
+ zod@4.3.6: {}
- zustand@5.0.9(@types/react@19.2.7)(react@19.2.1)(use-sync-external-store@1.6.0(react@19.2.1)):
+ zustand@5.0.11(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)):
optionalDependencies:
- '@types/react': 19.2.7
- react: 19.2.1
- use-sync-external-store: 1.6.0(react@19.2.1)
+ '@types/react': 19.2.14
+ react: 19.2.4
+ use-sync-external-store: 1.6.0(react@19.2.4)
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index b0e015b..4042287 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -1,4 +1,13 @@
packages:
- "benchmarks/*"
- "packages/*"
- - "www"
\ No newline at end of file
+ - "www"
+
+catalog:
+ "@types/node": "25.2.3"
+ "@types/react-dom": "19.2.3"
+ "@types/react": "19.2.14"
+ next: "16.1.6"
+ react-dom: "19.2.4"
+ react: "19.2.4"
+ typescript: "5.9.3"
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
index cdd162f..e0b296f 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,7 +1,11 @@
{
"compilerOptions": {
"target": "ESNext",
- "lib": ["DOM", "DOM.Iterable", "ESNext"],
+ "lib": [
+ "DOM",
+ "DOM.Iterable",
+ "ESNext"
+ ],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
@@ -15,6 +19,11 @@
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
- "include": ["packages/**/*.ts", "packages/**/*.tsx"],
- "exclude": ["node_modules"]
-}
\ No newline at end of file
+ "include": [
+ "packages/**/*.ts",
+ "packages/**/*.tsx"
+ ],
+ "exclude": [
+ "node_modules"
+ ]
+}
\ No newline at end of file
diff --git a/turbo.json b/turbo.json
index b3c5fe4..596540f 100644
--- a/turbo.json
+++ b/turbo.json
@@ -1,39 +1,28 @@
{
- "$schema": "https://turborepo.com/schema.json",
- "ui": "tui",
- "globalEnv": [
- "TURSO_DATABASE_URL",
- "TURSO_DATABASE_AUTH_TOKEN"
- ],
- "tasks": {
- "build": {
- "dependsOn": ["^build"],
- "inputs": ["$TURBO_DEFAULT$", ".env*"],
- "outputs": [".next/**", "!.next/cache/**", "dist/**"]
- },
- "@cookiebench/cli#build": {
- "inputs": ["src/**", "package.json", "rslib.config.ts", "tsconfig.json"],
- "outputs": ["dist/**"]
- },
- "@cookiebench/www#build": {
- "inputs": ["$TURBO_DEFAULT$", ".env*"],
- "outputs": [".next/**", "!.next/cache/**"]
- },
- "lint": {
- "dependsOn": ["^lint"]
- },
- "check-types": {
- "dependsOn": ["^check-types"]
- },
- "fmt": {
- "dependsOn": ["^fmt"]
- },
- "benchmark": {
- "dependsOn": ["^build", "^benchmark"]
- },
- "dev": {
- "cache": false,
- "persistent": true
- }
- }
+ "$schema": "https://turborepo.com/schema.json",
+ "ui": "tui",
+ "globalEnv": ["TURSO_DATABASE_URL", "TURSO_DATABASE_AUTH_TOKEN"],
+ "tasks": {
+ "build": {
+ "dependsOn": ["^build"],
+ "inputs": ["$TURBO_DEFAULT$", ".env*"],
+ "outputs": [".next/**", "!.next/cache/**", "dist/**"]
+ },
+ "lint": {
+ "dependsOn": ["^lint"]
+ },
+ "check-types": {
+ "dependsOn": ["^check-types"]
+ },
+ "fmt": {
+ "dependsOn": ["^fmt"]
+ },
+ "benchmark": {
+ "dependsOn": ["^build", "^benchmark"]
+ },
+ "dev": {
+ "cache": false,
+ "persistent": true
+ }
+ }
}
|