From 1f12b143ef5ce8286c5845c9bc7ad0296ebd11e4 Mon Sep 17 00:00:00 2001 From: Michael Mainguy Date: Wed, 6 Aug 2025 19:27:12 -0500 Subject: [PATCH] Initial commit: Performance Trace Analyzer with comprehensive features MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🚀 Features implemented: - IndexedDB local database for trace storage and management - Drag & drop trace file upload with validation - HTTP Request viewer with advanced filtering and analysis - CDN provider detection (Cloudflare, Akamai, CloudFront, etc.) - Queue time analysis with bottleneck detection - Visual highlighting for file sizes and request durations - Priority-based request analysis - Phase event viewer with detailed trace exploration - Filmstrip screenshot integration (with debugging) - 3D Babylon.js viewer component 📊 Analysis capabilities: - HTTP/1.1 vs HTTP/2 performance comparison - CDN edge vs origin detection - Connection limit bottleneck identification - Priority queue analysis - Visual correlation with network requests - Performance bottleneck identification 🛠️ Technical stack: - React 19.1.0 + TypeScript 5.8.3 - Vite build system - IndexedDB for local storage - Babylon.js 8+ for 3D visualization - Chrome DevTools trace format support 🎨 User experience: - Clean, professional interface design - Color-coded performance indicators - Expandable detailed views - Search and filtering capabilities - Responsive grid layouts - Intuitive navigation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .gitignore | 24 + CLAUDE.md | 68 + README.md | 69 + eslint.config.js | 23 + index.html | 12 + package-lock.json | 3383 ++++++++++++++++++++++++++ package.json | 30 + public/vite.svg | 1 + src/App.css | 1 + src/App.tsx | 207 ++ src/BabylonViewer.tsx | 77 + src/assets/react.svg | 1 + src/components/HTTPRequestViewer.tsx | 1518 ++++++++++++ src/components/PhaseViewer.tsx | 537 ++++ src/components/TraceSelector.tsx | 331 +++ src/components/TraceStats.tsx | 165 ++ src/components/TraceUpload.tsx | 247 ++ src/components/TraceViewer.tsx | 128 + src/hooks/useDatabaseTraceData.ts | 79 + src/hooks/useTraceData.ts | 60 + src/index.css | 1 + src/main.tsx | 10 + src/utils/traceDatabase.ts | 159 ++ src/utils/traceLoader.ts | 136 ++ src/vite-env.d.ts | 1 + tsconfig.app.json | 27 + tsconfig.json | 7 + tsconfig.node.json | 25 + types/trace.ts | 161 ++ vite.config.ts | 7 + 30 files changed, 7495 insertions(+) create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 README.md create mode 100644 eslint.config.js create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 public/vite.svg create mode 100644 src/App.css create mode 100644 src/App.tsx create mode 100644 src/BabylonViewer.tsx create mode 100644 src/assets/react.svg create mode 100644 src/components/HTTPRequestViewer.tsx create mode 100644 src/components/PhaseViewer.tsx create mode 100644 src/components/TraceSelector.tsx create mode 100644 src/components/TraceStats.tsx create mode 100644 src/components/TraceUpload.tsx create mode 100644 src/components/TraceViewer.tsx create mode 100644 src/hooks/useDatabaseTraceData.ts create mode 100644 src/hooks/useTraceData.ts create mode 100644 src/index.css create mode 100644 src/main.tsx create mode 100644 src/utils/traceDatabase.ts create mode 100644 src/utils/traceLoader.ts create mode 100644 src/vite-env.d.ts create mode 100644 tsconfig.app.json create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 types/trace.ts create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..9ff7d50 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,68 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is a React + TypeScript + Vite project called "perfviz" - a modern web application built with the latest versions of React (19.1.0), TypeScript (5.8.3), and Vite (7.0.4). + +## Development Commands + +- **Start development server**: `npm run dev` - Runs Vite dev server with HMR on http://localhost:5173/ +- **Build for production**: `npm run build` - TypeScript compilation followed by Vite build +- **Lint code**: `npm run lint` - Run ESLint on the entire codebase +- **Preview production build**: `npm run preview` - Preview the production build locally + +## Architecture + +### Build System +- **Vite** as the build tool with React plugin for Fast Refresh +- **ES modules** configuration (`"type": "module"` in package.json) +- **Bundler module resolution** with TypeScript extensions allowed in imports + +### TypeScript Configuration +- **Dual TypeScript configs**: + - `tsconfig.app.json` for application code (`src/` directory) + - `tsconfig.node.json` for build tooling (Vite config) +- **Strict TypeScript** with unused locals/parameters checking enabled +- **React JSX transform** (`react-jsx`) for React 19+ compatibility + +### Code Quality +- **ESLint** with TypeScript support and React-specific rules: + - React Hooks rules for proper hooks usage + - React Refresh rules for HMR compatibility + - TypeScript recommended rules +- **Globals configured** for browser environment + +### Application Structure +- **Entry point**: `src/main.tsx` renders the App component in React 19 StrictMode +- **Root component**: `src/App.tsx` contains the main application logic +- **3D Viewer**: `src/BabylonViewer.tsx` React component wrapping Babylon.js 3D scene +- **Styling**: CSS modules with `src/index.css` and `src/App.css` +- **Assets**: Static assets in `public/` and component assets in `src/assets/` + +## Key Dependencies + +- **React 19.1.0** with modern features and concurrent rendering +- **TypeScript 5.8.3** with latest language features +- **Vite 7.0.4** for fast development and optimized builds +- **ESLint 9.30.1** with typescript-eslint for code quality +- **BabylonJS 8.21.1** for 3D graphics and visualization + +## Development Notes + +- The project uses **React 19's new JSX transform** and concurrent features +- **HMR (Hot Module Replacement)** is enabled through Vite's React plugin +- **Strict mode** is enabled in development for better debugging +- **Browser targets** are ES2022+ for modern JavaScript features + +## Babylon.js Integration + +- **Version**: Using Babylon.js 8.21.1 (modern version 8+) +- **Camera Management**: Use `scene.activeCamera` instead of deprecated `camera.attachToCanvas()` +- **ArcRotateCamera Controls**: Use `camera.attachControl(canvas, true)` for mouse interaction +- **Scene Setup**: Cameras, lights, and meshes are added directly to the scene object +- **Engine Integration**: Engine handles canvas interaction and render loop +- **React Integration**: Proper cleanup with engine disposal in useEffect return function +- **Version-Specific Documentation**: ALWAYS check Babylon.js documentation for version 8.21.1 specifically to avoid deprecated methods and ensure current API usage +- **API Verification**: Before suggesting any Babylon.js code, verify method signatures and availability in the current version \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..7959ce4 --- /dev/null +++ b/README.md @@ -0,0 +1,69 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default tseslint.config([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + ...tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + ...tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + ...tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default tseslint.config([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..d94e7de --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { globalIgnores } from 'eslint/config' + +export default tseslint.config([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs['recommended-latest'], + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/index.html b/index.html new file mode 100644 index 0000000..b119459 --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + + + + + Perf Viz + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..4afb441 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3383 @@ +{ + "name": "perfviz", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "perfviz", + "version": "0.0.0", + "dependencies": { + "babylonjs": "^8.21.1", + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "@eslint/js": "^9.30.1", + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", + "@vitejs/plugin-react": "^4.6.0", + "eslint": "^9.30.1", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^16.3.0", + "typescript": "~5.8.3", + "typescript-eslint": "^8.35.1", + "vite": "^7.0.4" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", + "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.0", + "@babel/types": "^7.28.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz", + "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", + "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz", + "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz", + "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz", + "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz", + "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz", + "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz", + "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz", + "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz", + "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz", + "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz", + "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz", + "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz", + "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz", + "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz", + "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz", + "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz", + "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz", + "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz", + "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz", + "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz", + "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz", + "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz", + "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz", + "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz", + "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz", + "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", + "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.32.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.32.0.tgz", + "integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", + "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz", + "integrity": "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.2.tgz", + "integrity": "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.2.tgz", + "integrity": "sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.2.tgz", + "integrity": "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.2.tgz", + "integrity": "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.2.tgz", + "integrity": "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.2.tgz", + "integrity": "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.2.tgz", + "integrity": "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.2.tgz", + "integrity": "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.2.tgz", + "integrity": "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.2.tgz", + "integrity": "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.2.tgz", + "integrity": "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.2.tgz", + "integrity": "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.2.tgz", + "integrity": "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.2.tgz", + "integrity": "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz", + "integrity": "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.2.tgz", + "integrity": "sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.2.tgz", + "integrity": "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.2.tgz", + "integrity": "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.2.tgz", + "integrity": "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.1.9", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.9.tgz", + "integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.1.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.7.tgz", + "integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.0.tgz", + "integrity": "sha512-bhEz6OZeUR+O/6yx9Jk6ohX6H9JSFTaiY0v9/PuKT3oGK0rn0jNplLmyFUGV+a9gfYnVNwGDwS/UkLIuXNb2Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.39.0", + "@typescript-eslint/type-utils": "8.39.0", + "@typescript-eslint/utils": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.39.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.0.tgz", + "integrity": "sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.39.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.0.tgz", + "integrity": "sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.39.0", + "@typescript-eslint/types": "^8.39.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.0.tgz", + "integrity": "sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.0.tgz", + "integrity": "sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.39.0.tgz", + "integrity": "sha512-6B3z0c1DXVT2vYA9+z9axjtc09rqKUPRmijD5m9iv8iQpHBRYRMBcgxSiKTZKm6FwWw1/cI4v6em35OsKCiN5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0", + "@typescript-eslint/utils": "8.39.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.0.tgz", + "integrity": "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.0.tgz", + "integrity": "sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.39.0", + "@typescript-eslint/tsconfig-utils": "8.39.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.0.tgz", + "integrity": "sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.39.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.0.tgz", + "integrity": "sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.39.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/babylonjs": { + "version": "8.21.1", + "resolved": "https://registry.npmjs.org/babylonjs/-/babylonjs-8.21.1.tgz", + "integrity": "sha512-Hc+5J8CTMAw1xZxb2kvC7KWTphkD59c/jHmgMijEF7IWzQyqYcRK+HKJrlj0PRozKmibChRfNlxynIVLhr5tXA==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001726", + "electron-to-chromium": "^1.5.173", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001731", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz", + "integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.197", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.197.tgz", + "integrity": "sha512-m1xWB3g7vJ6asIFz+2pBUbq3uGmfmln1M9SSvBe4QIFWYrRHylP73zL/3nMjDmwz8V+1xAXQDfBd6+HPW0WvDQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", + "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.8", + "@esbuild/android-arm": "0.25.8", + "@esbuild/android-arm64": "0.25.8", + "@esbuild/android-x64": "0.25.8", + "@esbuild/darwin-arm64": "0.25.8", + "@esbuild/darwin-x64": "0.25.8", + "@esbuild/freebsd-arm64": "0.25.8", + "@esbuild/freebsd-x64": "0.25.8", + "@esbuild/linux-arm": "0.25.8", + "@esbuild/linux-arm64": "0.25.8", + "@esbuild/linux-ia32": "0.25.8", + "@esbuild/linux-loong64": "0.25.8", + "@esbuild/linux-mips64el": "0.25.8", + "@esbuild/linux-ppc64": "0.25.8", + "@esbuild/linux-riscv64": "0.25.8", + "@esbuild/linux-s390x": "0.25.8", + "@esbuild/linux-x64": "0.25.8", + "@esbuild/netbsd-arm64": "0.25.8", + "@esbuild/netbsd-x64": "0.25.8", + "@esbuild/openbsd-arm64": "0.25.8", + "@esbuild/openbsd-x64": "0.25.8", + "@esbuild/openharmony-arm64": "0.25.8", + "@esbuild/sunos-x64": "0.25.8", + "@esbuild/win32-arm64": "0.25.8", + "@esbuild/win32-ia32": "0.25.8", + "@esbuild/win32-x64": "0.25.8" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz", + "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.0", + "@eslint/core": "^0.15.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.32.0", + "@eslint/plugin-kit": "^0.3.4", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.20", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz", + "integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", + "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", + "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", + "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz", + "integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.46.2", + "@rollup/rollup-android-arm64": "4.46.2", + "@rollup/rollup-darwin-arm64": "4.46.2", + "@rollup/rollup-darwin-x64": "4.46.2", + "@rollup/rollup-freebsd-arm64": "4.46.2", + "@rollup/rollup-freebsd-x64": "4.46.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.46.2", + "@rollup/rollup-linux-arm-musleabihf": "4.46.2", + "@rollup/rollup-linux-arm64-gnu": "4.46.2", + "@rollup/rollup-linux-arm64-musl": "4.46.2", + "@rollup/rollup-linux-loongarch64-gnu": "4.46.2", + "@rollup/rollup-linux-ppc64-gnu": "4.46.2", + "@rollup/rollup-linux-riscv64-gnu": "4.46.2", + "@rollup/rollup-linux-riscv64-musl": "4.46.2", + "@rollup/rollup-linux-s390x-gnu": "4.46.2", + "@rollup/rollup-linux-x64-gnu": "4.46.2", + "@rollup/rollup-linux-x64-musl": "4.46.2", + "@rollup/rollup-win32-arm64-msvc": "4.46.2", + "@rollup/rollup-win32-ia32-msvc": "4.46.2", + "@rollup/rollup-win32-x64-msvc": "4.46.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.39.0.tgz", + "integrity": "sha512-lH8FvtdtzcHJCkMOKnN73LIn6SLTpoojgJqDAxPm1jCR14eWSGPX8ul/gggBdPMk/d5+u9V854vTYQ8T5jF/1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.39.0", + "@typescript-eslint/parser": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0", + "@typescript-eslint/utils": "8.39.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.6.tgz", + "integrity": "sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.6", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.40.0", + "tinyglobby": "^0.2.14" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3542a4b --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "perfviz", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "babylonjs": "^8.21.1", + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "@eslint/js": "^9.30.1", + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", + "@vitejs/plugin-react": "^4.6.0", + "eslint": "^9.30.1", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^16.3.0", + "typescript": "~5.8.3", + "typescript-eslint": "^8.35.1", + "vite": "^7.0.4" + } +} diff --git a/public/vite.svg b/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/App.css b/src/App.css new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/App.css @@ -0,0 +1 @@ + diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..7cb3e14 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,207 @@ +import { useState, useEffect } from 'react' +import './App.css' +import BabylonViewer from './BabylonViewer' +import TraceViewer from './components/TraceViewer' +import PhaseViewer from './components/PhaseViewer' +import HTTPRequestViewer from './components/HTTPRequestViewer' +import TraceUpload from './components/TraceUpload' +import TraceSelector from './components/TraceSelector' +import { traceDatabase } from './utils/traceDatabase' + +type AppView = '3d' | 'trace' | 'phases' | 'http' +type AppMode = 'selector' | 'upload' | 'analysis' + +function App() { + const [mode, setMode] = useState('selector') + const [currentView, setCurrentView] = useState('http') + const [selectedTraceId, setSelectedTraceId] = useState(null) + const [hasTraces, setHasTraces] = useState(false) + const [dbInitialized, setDbInitialized] = useState(false) + + useEffect(() => { + initializeApp() + }, []) + + const initializeApp = async () => { + try { + // Initialize the database + await traceDatabase.init() + setDbInitialized(true) + + // Check if we have any traces + const traces = await traceDatabase.getAllTraces() + setHasTraces(traces.length > 0) + + // If no traces, show upload screen + if (traces.length === 0) { + setMode('upload') + } else { + setMode('selector') + } + } catch (error) { + console.error('Failed to initialize database:', error) + setMode('upload') // Fallback to upload mode + setDbInitialized(true) + } + } + + const handleTraceSelect = (traceId: string) => { + setSelectedTraceId(traceId) + setMode('analysis') + } + + const handleUploadSuccess = (traceId: string) => { + setSelectedTraceId(traceId) + setMode('analysis') + setHasTraces(true) + } + + const handleBackToSelector = () => { + setSelectedTraceId(null) + setMode('selector') + } + + const handleUploadNew = () => { + setMode('upload') + } + + if (!dbInitialized) { + return ( +
+
Initializing database...
+
+ ) + } + + if (mode === 'upload') { + return + } + + if (mode === 'selector') { + return ( + + ) + } + + // Analysis mode - show the main interface + return ( + <> +
+
+
+ +

Perf Viz

+
+ + +
+ + {currentView === '3d' && ( +
+ +
+ )} + + {currentView === 'trace' && ( + + )} + + {currentView === 'phases' && ( + + )} + + {currentView === 'http' && ( + + )} +
+ + ) +} + +export default App diff --git a/src/BabylonViewer.tsx b/src/BabylonViewer.tsx new file mode 100644 index 0000000..fb90938 --- /dev/null +++ b/src/BabylonViewer.tsx @@ -0,0 +1,77 @@ +import { useEffect, useRef } from 'react' +import { + Engine, + Scene, + ArcRotateCamera, + Vector3, + HemisphericLight, + MeshBuilder, + StandardMaterial, + Color3 +} from 'babylonjs' + +interface BabylonViewerProps { + width?: number + height?: number +} + +export default function BabylonViewer({ width = 800, height = 600 }: BabylonViewerProps) { + const canvasRef = useRef(null) + const engineRef = useRef(null) + const sceneRef = useRef(null) + + useEffect(() => { + if (!canvasRef.current) return + + const canvas = canvasRef.current + const engine = new Engine(canvas, true) + engineRef.current = engine + + const scene = new Scene(engine) + sceneRef.current = scene + + const camera = new ArcRotateCamera('camera1', -Math.PI / 2, Math.PI / 2.5, 10, Vector3.Zero(), scene) + camera.attachControl(canvas, true) + scene.activeCamera = camera + + const light = new HemisphericLight('light', new Vector3(0, 1, 0), scene) + light.intensity = 0.7 + + const sphere = MeshBuilder.CreateSphere('sphere', { diameter: 2 }, scene) + sphere.position.y = 1 + + const ground = MeshBuilder.CreateGround('ground', { width: 6, height: 6 }, scene) + + const sphereMaterial = new StandardMaterial('sphereMaterial', scene) + sphereMaterial.diffuseColor = new Color3(1, 0, 1) + sphere.material = sphereMaterial + + const groundMaterial = new StandardMaterial('groundMaterial', scene) + groundMaterial.diffuseColor = new Color3(0.5, 0.5, 0.5) + ground.material = groundMaterial + + engine.runRenderLoop(() => { + scene.render() + }) + + const handleResize = () => { + engine.resize() + } + + window.addEventListener('resize', handleResize) + + return () => { + window.removeEventListener('resize', handleResize) + engine.dispose() + } + }, []) + + return ( + + ) +} \ No newline at end of file diff --git a/src/assets/react.svg b/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/HTTPRequestViewer.tsx b/src/components/HTTPRequestViewer.tsx new file mode 100644 index 0000000..a96d679 --- /dev/null +++ b/src/components/HTTPRequestViewer.tsx @@ -0,0 +1,1518 @@ +import { useState, useMemo } from 'react' +import { useDatabaseTraceData } from '../hooks/useDatabaseTraceData' +import type { TraceEvent } from '../../types/trace' + +interface QueueAnalysis { + reason: 'connection_limit' | 'priority_queue' | 'resource_contention' | 'unknown' + description: string + concurrentRequests: number + connectionId?: number + relatedRequests?: string[] +} + +interface CDNAnalysis { + provider: 'cloudflare' | 'akamai' | 'aws_cloudfront' | 'fastly' | 'azure_cdn' | 'google_cdn' | 'cdn77' | 'keycdn' | 'unknown' + isEdge: boolean + cacheStatus?: 'hit' | 'miss' | 'expired' | 'bypass' | 'unknown' + edgeLocation?: string + confidence: number // 0-1 confidence score + detectionMethod: string // How we detected it +} + +interface ScreenshotEvent { + timestamp: number + screenshot: string // base64 image data + index: number +} + +interface HTTPRequest { + requestId: string + url: string + hostname: string + method: string + resourceType: string + priority: string + statusCode?: number + mimeType?: string + protocol?: string + events: { + willSendRequest?: TraceEvent + sendRequest?: TraceEvent + receiveResponse?: TraceEvent + receivedData?: TraceEvent[] + finishLoading?: TraceEvent + } + timing: { + start: number + startOffset?: number + end?: number + duration?: number + queueTime?: number + networkDuration?: number + dnsStart?: number + dnsEnd?: number + connectStart?: number + connectEnd?: number + sslStart?: number + sslEnd?: number + sendStart?: number + sendEnd?: number + receiveHeadersStart?: number + receiveHeadersEnd?: number + } + responseHeaders?: Array<{ name: string, value: string }> + encodedDataLength?: number + fromCache: boolean + connectionReused: boolean + queueAnalysis?: QueueAnalysis + cdnAnalysis?: CDNAnalysis +} + +const ITEMS_PER_PAGE = 25 + +// Global highlighting constants +const HIGHLIGHTING_CONFIG = { + // File size thresholds (in KB) + SIZE_THRESHOLDS: { + LARGE: 500, // Red highlighting for files >= 500KB + MEDIUM: 100, // Yellow highlighting for files >= 100KB + SMALL: 50 // Green highlighting for files <= 50KB + }, + + // Duration thresholds (in milliseconds) + DURATION_THRESHOLDS: { + SLOW: 250, // Red highlighting for requests > 150ms + MEDIUM: 100, // Yellow highlighting for requests 50-150ms + FAST: 100 // Green highlighting for requests < 50ms + }, + + // Queue time thresholds (in microseconds) + QUEUE_TIME: { + HIGH_THRESHOLD: 10000, // 10ms - highlight in red and show analysis + ANALYSIS_THRESHOLD: 10000 // 10ms - minimum queue time for analysis + }, + + // Colors + COLORS: { + // Status colors + STATUS: { + SUCCESS: '#28a745', // 2xx + REDIRECT: '#ffc107', // 3xx + CLIENT_ERROR: '#fd7e14', // 4xx + SERVER_ERROR: '#dc3545', // 5xx + UNKNOWN: '#6c757d' + }, + + // Protocol colors + PROTOCOL: { + HTTP1_1: '#dc3545', // Red + H2: '#b8860b', // Dark yellow + H3: '#006400', // Dark green + UNKNOWN: '#6c757d' + }, + + // Priority colors + PRIORITY: { + VERY_HIGH: '#dc3545', // Red + HIGH: '#fd7e14', // Orange + MEDIUM: '#ffc107', // Yellow + LOW: '#28a745', // Green + VERY_LOW: '#6c757d', // Gray + UNKNOWN: '#6c757d' + }, + + // File size colors + SIZE: { + LARGE: { background: '#dc3545', color: 'white' }, // Red for 500KB+ + MEDIUM: { background: '#ffc107', color: 'black' }, // Yellow for 100-500KB + SMALL: { background: '#28a745', color: 'white' }, // Green for under 50KB + DEFAULT: { background: 'transparent', color: '#495057' } // Default for 50-100KB + }, + + // Duration colors + DURATION: { + SLOW: { background: '#dc3545', color: 'white' }, // Red for > 150ms + MEDIUM: { background: '#ffc107', color: 'black' }, // Yellow for 50-150ms + FAST: { background: '#28a745', color: 'white' }, // Green for < 50ms + DEFAULT: { background: 'transparent', color: '#495057' } // Default + } + } +} + +const getHostnameFromUrl = (url: string): string => { + try { + const urlObj = new URL(url) + return urlObj.hostname + } catch { + return 'unknown' + } +} + +const extractScreenshots = (traceEvents: TraceEvent[]): ScreenshotEvent[] => { + const screenshots: ScreenshotEvent[] = [] + let index = 0 + + // Debug: Check for any screenshot-related events + const screenshotRelated = traceEvents.filter(event => + event.name.toLowerCase().includes('screenshot') || + event.cat.toLowerCase().includes('screenshot') || + event.name.toLowerCase().includes('snap') || + event.name.toLowerCase().includes('frame') || + event.cat.toLowerCase().includes('devtools') + ) + + console.log('Debug: Screenshot-related events found:', screenshotRelated.length) + if (screenshotRelated.length > 0) { + console.log('Debug: Screenshot-related sample:', screenshotRelated.slice(0, 5).map(e => ({ + name: e.name, + cat: e.cat, + hasArgs: !!e.args, + argsKeys: e.args ? Object.keys(e.args) : [] + }))) + } + + for (const event of traceEvents) { + // Look for screenshot events with more flexible matching + if ((event.name === 'Screenshot' || event.name === 'screenshot') && + (event.cat === 'disabled-by-default-devtools.screenshot' || + event.cat.includes('screenshot') || + event.cat.includes('devtools')) && + event.args) { + const args = event.args as any + + // Check multiple possible locations for screenshot data + const snapshot = args.snapshot || args.data?.snapshot || args.image || args.data?.image + + if (snapshot && typeof snapshot === 'string') { + // Accept both data URLs and base64 strings + if (snapshot.startsWith('data:image/') || snapshot.startsWith('/9j/') || snapshot.length > 1000) { + const screenshotData = snapshot.startsWith('data:image/') ? snapshot : `data:image/jpeg;base64,${snapshot}` + screenshots.push({ + timestamp: event.ts, + screenshot: screenshotData, + index: index++ + }) + } + } + } + } + + console.log('Debug: Extracted screenshots:', screenshots.length) + return screenshots.sort((a, b) => a.timestamp - b.timestamp) +} + +const findUniqueScreenshots = (screenshots: ScreenshotEvent[]): ScreenshotEvent[] => { + if (screenshots.length === 0) return [] + + const uniqueScreenshots: ScreenshotEvent[] = [screenshots[0]] // Always include first screenshot + + // Compare each screenshot with the previous unique one + for (let i = 1; i < screenshots.length; i++) { + const current = screenshots[i] + const lastUnique = uniqueScreenshots[uniqueScreenshots.length - 1] + + // Simple comparison - if the base64 data is different, it's a new screenshot + // For better performance on large images, we could compare just a hash or portion + if (current.screenshot !== lastUnique.screenshot) { + uniqueScreenshots.push(current) + } + } + + return uniqueScreenshots +} + +const analyzeCDN = (request: HTTPRequest): CDNAnalysis => { + const hostname = request.hostname + const headers = request.responseHeaders || [] + const headerMap = new Map(headers.map(h => [h.name.toLowerCase(), h.value.toLowerCase()])) + + let provider: CDNAnalysis['provider'] = 'unknown' + let isEdge = false + let cacheStatus: CDNAnalysis['cacheStatus'] = 'unknown' + let edgeLocation: string | undefined + let confidence = 0 + let detectionMethod = 'unknown' + + // Cloudflare detection + if (headerMap.has('cf-ray') || headerMap.has('cf-cache-status') || hostname.includes('cloudflare')) { + provider = 'cloudflare' + confidence = 0.95 + detectionMethod = 'headers (cf-ray/cf-cache-status)' + isEdge = true + + const cfCacheStatus = headerMap.get('cf-cache-status') + if (cfCacheStatus) { + if (cfCacheStatus.includes('hit')) cacheStatus = 'hit' + else if (cfCacheStatus.includes('miss')) cacheStatus = 'miss' + else if (cfCacheStatus.includes('expired')) cacheStatus = 'expired' + else if (cfCacheStatus.includes('bypass')) cacheStatus = 'bypass' + } + + edgeLocation = headerMap.get('cf-ray')?.split('-')[1] + } + + // Akamai detection + else if (headerMap.has('akamai-cache-status') || headerMap.has('x-cache-key') || + hostname.includes('akamai') || headerMap.get('server')?.includes('akamai')) { + provider = 'akamai' + confidence = 0.9 + detectionMethod = 'headers (akamai-cache-status/server)' + isEdge = true + + const akamaiStatus = headerMap.get('akamai-cache-status') + if (akamaiStatus) { + if (akamaiStatus.includes('hit')) cacheStatus = 'hit' + else if (akamaiStatus.includes('miss')) cacheStatus = 'miss' + } + } + + // AWS CloudFront detection + else if (headerMap.has('x-amz-cf-id') || headerMap.has('x-amz-cf-pop') || + hostname.includes('cloudfront.net')) { + provider = 'aws_cloudfront' + confidence = 0.95 + detectionMethod = 'headers (x-amz-cf-id/x-amz-cf-pop)' + isEdge = true + + const cloudfrontCache = headerMap.get('x-cache') + if (cloudfrontCache) { + if (cloudfrontCache.includes('hit')) cacheStatus = 'hit' + else if (cloudfrontCache.includes('miss')) cacheStatus = 'miss' + } + + edgeLocation = headerMap.get('x-amz-cf-pop') + } + + // Fastly detection + else if (headerMap.has('fastly-debug-digest') || headerMap.has('x-served-by') || + hostname.includes('fastly.com') || headerMap.get('via')?.includes('fastly')) { + provider = 'fastly' + confidence = 0.85 + detectionMethod = 'headers (fastly-debug-digest/x-served-by/via)' + isEdge = true + + const fastlyCache = headerMap.get('x-cache') + if (fastlyCache) { + if (fastlyCache.includes('hit')) cacheStatus = 'hit' + else if (fastlyCache.includes('miss')) cacheStatus = 'miss' + } + } + + // Azure CDN detection + else if (headerMap.has('x-azure-ref') || hostname.includes('azureedge.net')) { + provider = 'azure_cdn' + confidence = 0.9 + detectionMethod = 'headers (x-azure-ref) or hostname' + isEdge = true + } + + // Google CDN detection + else if (headerMap.has('x-goog-generation') || hostname.includes('googleapis.com') || + headerMap.get('server')?.includes('gws')) { + provider = 'google_cdn' + confidence = 0.8 + detectionMethod = 'headers (x-goog-generation) or server' + isEdge = true + } + + // Generic CDN patterns + else if (hostname.includes('cdn') || hostname.includes('cache') || + hostname.includes('edge') || hostname.includes('static')) { + provider = 'unknown' + confidence = 0.3 + detectionMethod = 'hostname patterns (cdn/cache/edge/static)' + isEdge = true + } + + // Check for origin indicators (lower confidence for edge) + if (hostname.includes('origin') || hostname.includes('api') || + headerMap.get('x-cache')?.includes('miss from origin')) { + isEdge = false + confidence = Math.max(0.1, confidence - 0.2) + detectionMethod += ' + origin indicators' + } + + return { + provider, + isEdge, + cacheStatus, + edgeLocation, + confidence, + detectionMethod + } +} + +const analyzeQueueReason = (request: HTTPRequest, allRequests: HTTPRequest[]): QueueAnalysis => { + const queueTime = request.timing.queueTime || 0 + + // If no significant queue time, no analysis needed + if (queueTime < HIGHLIGHTING_CONFIG.QUEUE_TIME.ANALYSIS_THRESHOLD) { + return { + reason: 'unknown', + description: 'No significant queueing detected', + concurrentRequests: 0 + } + } + + const requestStart = request.timing.start + const requestEnd = request.timing.end || requestStart + (request.timing.duration || 0) + + // Find concurrent requests to the same host + const concurrentToSameHost = allRequests.filter(other => { + if (other.requestId === request.requestId) return false + if (other.hostname !== request.hostname) return false + + const otherStart = other.timing.start + const otherEnd = other.timing.end || otherStart + (other.timing.duration || 0) + + // Check if requests overlap in time + return (otherStart <= requestEnd && otherEnd >= requestStart) + }) + + const concurrentSameProtocol = concurrentToSameHost.filter(r => r.protocol === request.protocol) + + // HTTP/1.1 connection limit analysis (typically 6 connections per host) + if (request.protocol === 'http/1.1' && concurrentSameProtocol.length >= 6) { + return { + reason: 'connection_limit', + description: `HTTP/1.1 connection limit reached (${concurrentSameProtocol.length} concurrent requests)`, + concurrentRequests: concurrentSameProtocol.length, + relatedRequests: concurrentSameProtocol.slice(0, 5).map(r => r.requestId) + } + } + + // H2 multiplexing but still queued - likely priority or server limits + if (request.protocol === 'h2' && concurrentSameProtocol.length > 0) { + // Check if lower priority requests are being processed first + const lowerPriorityActive = concurrentSameProtocol.filter(r => { + const priorityOrder = ['VeryLow', 'Low', 'Medium', 'High', 'VeryHigh'] + const requestPriorityIndex = priorityOrder.indexOf(request.priority || 'Medium') + const otherPriorityIndex = priorityOrder.indexOf(r.priority || 'Medium') + return otherPriorityIndex < requestPriorityIndex + }) + + if (lowerPriorityActive.length > 0) { + return { + reason: 'priority_queue', + description: `Queued behind ${lowerPriorityActive.length} lower priority requests`, + concurrentRequests: concurrentSameProtocol.length, + relatedRequests: lowerPriorityActive.slice(0, 3).map(r => r.requestId) + } + } + + return { + reason: 'resource_contention', + description: `Server resource contention (${concurrentSameProtocol.length} concurrent H2 requests)`, + concurrentRequests: concurrentSameProtocol.length, + relatedRequests: concurrentSameProtocol.slice(0, 3).map(r => r.requestId) + } + } + + // General resource contention + if (concurrentToSameHost.length > 0) { + return { + reason: 'resource_contention', + description: `Resource contention with ${concurrentToSameHost.length} concurrent requests`, + concurrentRequests: concurrentToSameHost.length, + relatedRequests: concurrentToSameHost.slice(0, 3).map(r => r.requestId) + } + } + + return { + reason: 'unknown', + description: `Queued for ${(queueTime / 1000).toFixed(1)}ms - reason unclear`, + concurrentRequests: 0 + } +} + +interface HTTPRequestViewerProps { + traceId: string | null +} + +export default function HTTPRequestViewer({ traceId }: HTTPRequestViewerProps) { + const { traceData, loading, error } = useDatabaseTraceData(traceId) + const [currentPage, setCurrentPage] = useState(1) + const [searchTerm, setSearchTerm] = useState('') + const [resourceTypeFilter, setResourceTypeFilter] = useState('all') + const [protocolFilter, setProtocolFilter] = useState('all') + const [hostnameFilter, setHostnameFilter] = useState('all') + const [priorityFilter, setPriorityFilter] = useState('all') + const [showQueueAnalysis, setShowQueueAnalysis] = useState(false) + const [showScreenshots, setShowScreenshots] = useState(false) + const [expandedRows, setExpandedRows] = useState>(new Set()) + + const httpRequests = useMemo(() => { + if (!traceData) return [] + + const requestsMap = new Map() + + // Process all events and group by requestId + for (const event of traceData.traceEvents) { + const args = event.args as any + const requestId = args?.data?.requestId + + if (!requestId) continue + + if (!requestsMap.has(requestId)) { + requestsMap.set(requestId, { + requestId, + url: '', + hostname: '', + method: '', + resourceType: '', + priority: '', + events: {}, + timing: { start: event.ts }, + fromCache: false, + connectionReused: false + }) + } + + const request = requestsMap.get(requestId)! + + switch (event.name) { + case 'ResourceWillSendRequest': + request.events.willSendRequest = event + request.timing.start = Math.min(request.timing.start, event.ts) + break + + case 'ResourceSendRequest': + request.events.sendRequest = event + request.url = args.data.url || '' + request.hostname = getHostnameFromUrl(request.url) + request.method = args.data.requestMethod || '' + request.resourceType = args.data.resourceType || '' + request.priority = args.data.priority || '' + request.timing.start = Math.min(request.timing.start, event.ts) + break + + case 'ResourceReceiveResponse': + request.events.receiveResponse = event + request.statusCode = args.data.statusCode + request.mimeType = args.data.mimeType + request.protocol = args.data.protocol + request.responseHeaders = args.data.headers + request.fromCache = args.data.fromCache || false + request.connectionReused = args.data.connectionReused || false + request.timing.end = event.ts + + // Extract network timing details + const timing = args.data.timing + if (timing) { + request.timing.networkDuration = timing.receiveHeadersEnd + request.timing.dnsStart = timing.dnsStart + request.timing.dnsEnd = timing.dnsEnd + request.timing.connectStart = timing.connectStart + request.timing.connectEnd = timing.connectEnd + request.timing.sslStart = timing.sslStart + request.timing.sslEnd = timing.sslEnd + request.timing.sendStart = timing.sendStart + request.timing.sendEnd = timing.sendEnd + request.timing.receiveHeadersStart = timing.receiveHeadersStart + request.timing.receiveHeadersEnd = timing.receiveHeadersEnd + } + break + + case 'ResourceReceivedData': + if (!request.events.receivedData) request.events.receivedData = [] + request.events.receivedData.push(event) + request.encodedDataLength = (request.encodedDataLength || 0) + (args.data.encodedDataLength || 0) + request.timing.end = Math.max(request.timing.end || event.ts, event.ts) + break + + case 'ResourceFinishLoading': + request.events.finishLoading = event + request.timing.end = event.ts + break + } + + // Calculate total duration + if (request.timing.end) { + request.timing.duration = request.timing.end - request.timing.start + } + } + + // Calculate queue times and filter valid requests + const sortedRequests = Array.from(requestsMap.values()) + .filter(req => req.url && req.method) // Only include valid requests + .map(request => { + // Calculate queue time using multiple approaches + if (request.events.willSendRequest && request.events.sendRequest) { + // Primary approach: ResourceSendRequest - ResourceWillSendRequest + request.timing.queueTime = request.events.sendRequest.ts - request.events.willSendRequest.ts + } else if (request.events.sendRequest && request.events.receiveResponse) { + // Fallback approach: use timing data from ResourceReceiveResponse + const responseArgs = request.events.receiveResponse.args as any + const timingData = responseArgs?.data?.timing + if (timingData?.requestTime) { + // requestTime is in seconds, convert to microseconds + const requestTimeUs = timingData.requestTime * 1000000 + // Queue time = requestTime (when browser queued) - ResourceSendRequest timestamp (when DevTools recorded) + // Note: requestTime is the actual browser queue time, ResourceSendRequest is the DevTools event + request.timing.queueTime = requestTimeUs - request.events.sendRequest.ts + } + } + return request + }) + .sort((a, b) => a.timing.start - b.timing.start) + + // Calculate start time offsets from the first request + if (sortedRequests.length > 0) { + const firstRequestTime = sortedRequests[0].timing.start + sortedRequests.forEach(request => { + request.timing.startOffset = request.timing.start - firstRequestTime + }) + } + + // Add queue analysis for requests with significant queue time + sortedRequests.forEach(request => { + if ((request.timing.queueTime || 0) >= HIGHLIGHTING_CONFIG.QUEUE_TIME.ANALYSIS_THRESHOLD) { + request.queueAnalysis = analyzeQueueReason(request, sortedRequests) + } + }) + + // Add CDN analysis for all requests + sortedRequests.forEach(request => { + if (request.responseHeaders && request.responseHeaders.length > 0) { + request.cdnAnalysis = analyzeCDN(request) + } + }) + + return sortedRequests + }, [traceData]) + + // Extract and process screenshots + const screenshots = useMemo(() => { + if (!traceData) return [] + + const allScreenshots = extractScreenshots(traceData.traceEvents) + console.log('Debug: Found screenshots:', allScreenshots.length) + console.log('Debug: Screenshot events sample:', allScreenshots.slice(0, 3)) + + const uniqueScreenshots = findUniqueScreenshots(allScreenshots) + console.log('Debug: Unique screenshots:', uniqueScreenshots.length) + + return uniqueScreenshots + }, [traceData]) + + const filteredRequests = useMemo(() => { + let requests = httpRequests + + // Filter by resource type + if (resourceTypeFilter !== 'all') { + requests = requests.filter(req => req.resourceType === resourceTypeFilter) + } + + // Filter by protocol + if (protocolFilter !== 'all') { + requests = requests.filter(req => req.protocol === protocolFilter) + } + + // Filter by hostname + if (hostnameFilter !== 'all') { + requests = requests.filter(req => req.hostname === hostnameFilter) + } + + // Filter by priority + if (priorityFilter !== 'all') { + requests = requests.filter(req => req.priority === priorityFilter) + } + + // Filter by search term + if (searchTerm) { + const term = searchTerm.toLowerCase() + requests = requests.filter(req => + req.url.toLowerCase().includes(term) || + req.method.toLowerCase().includes(term) || + req.resourceType.toLowerCase().includes(term) || + req.hostname.toLowerCase().includes(term) || + req.statusCode?.toString().includes(term) + ) + } + + return requests + }, [httpRequests, resourceTypeFilter, protocolFilter, hostnameFilter, priorityFilter, searchTerm]) + + const paginatedRequests = useMemo(() => { + const startIndex = (currentPage - 1) * ITEMS_PER_PAGE + return filteredRequests.slice(startIndex, startIndex + ITEMS_PER_PAGE) + }, [filteredRequests, currentPage]) + + // Create timeline entries that include both HTTP requests and screenshot changes + const timelineEntries = useMemo(() => { + const entries: Array<{ + type: 'request' | 'screenshot', + timestamp: number, + data: HTTPRequest | ScreenshotEvent + }> = [] + + // Add HTTP requests + filteredRequests.forEach(request => { + entries.push({ + type: 'request', + timestamp: request.timing.start, + data: request + }) + }) + + // Add screenshots if enabled + if (showScreenshots && screenshots.length > 0) { + screenshots.forEach(screenshot => { + entries.push({ + type: 'screenshot', + timestamp: screenshot.timestamp, + data: screenshot + }) + }) + } + + // Sort by timestamp + return entries.sort((a, b) => a.timestamp - b.timestamp) + }, [filteredRequests, screenshots, showScreenshots]) + + const paginatedTimelineEntries = useMemo(() => { + const startIndex = (currentPage - 1) * ITEMS_PER_PAGE + return timelineEntries.slice(startIndex, startIndex + ITEMS_PER_PAGE) + }, [timelineEntries, currentPage]) + + const resourceTypes = useMemo(() => { + const types = new Set(httpRequests.map(req => req.resourceType)) + return Array.from(types).sort() + }, [httpRequests]) + + const protocols = useMemo(() => { + const protos = new Set(httpRequests.map(req => req.protocol).filter(Boolean)) + return Array.from(protos).sort() + }, [httpRequests]) + + const hostnames = useMemo(() => { + const hosts = new Set(httpRequests.map(req => req.hostname).filter(h => h !== 'unknown')) + return Array.from(hosts).sort() + }, [httpRequests]) + + const priorities = useMemo(() => { + const prios = new Set(httpRequests.map(req => req.priority).filter(Boolean)) + return Array.from(prios).sort((a, b) => { + // Sort priorities by importance: VeryHigh, High, Medium, Low, VeryLow + const order = ['VeryHigh', 'High', 'Medium', 'Low', 'VeryLow'] + return order.indexOf(a) - order.indexOf(b) + }) + }, [httpRequests]) + + const totalPages = Math.ceil((showScreenshots ? timelineEntries.length : filteredRequests.length) / ITEMS_PER_PAGE) + + const formatDuration = (microseconds?: number) => { + if (!microseconds) return '-' + if (microseconds < 1000) return `${microseconds.toFixed(0)}μs` + return `${(microseconds / 1000).toFixed(2)}ms` + } + + const formatSize = (bytes?: number) => { + if (!bytes) return '-' + if (bytes < 1024) return `${bytes}B` + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB` + return `${(bytes / (1024 * 1024)).toFixed(1)}MB` + } + + const getStatusColor = (status?: number) => { + if (!status) return HIGHLIGHTING_CONFIG.COLORS.STATUS.UNKNOWN + if (status < 300) return HIGHLIGHTING_CONFIG.COLORS.STATUS.SUCCESS + if (status < 400) return HIGHLIGHTING_CONFIG.COLORS.STATUS.REDIRECT + if (status < 500) return HIGHLIGHTING_CONFIG.COLORS.STATUS.CLIENT_ERROR + return HIGHLIGHTING_CONFIG.COLORS.STATUS.SERVER_ERROR + } + + const getProtocolColor = (protocol?: string) => { + if (!protocol) return HIGHLIGHTING_CONFIG.COLORS.PROTOCOL.UNKNOWN + if (protocol === 'http/1.1') return HIGHLIGHTING_CONFIG.COLORS.PROTOCOL.HTTP1_1 + if (protocol === 'h2') return HIGHLIGHTING_CONFIG.COLORS.PROTOCOL.H2 + if (protocol === 'h3') return HIGHLIGHTING_CONFIG.COLORS.PROTOCOL.H3 + return HIGHLIGHTING_CONFIG.COLORS.PROTOCOL.UNKNOWN + } + + const getPriorityColor = (priority?: string) => { + if (!priority) return HIGHLIGHTING_CONFIG.COLORS.PRIORITY.UNKNOWN + if (priority === 'VeryHigh') return HIGHLIGHTING_CONFIG.COLORS.PRIORITY.VERY_HIGH + if (priority === 'High') return HIGHLIGHTING_CONFIG.COLORS.PRIORITY.HIGH + if (priority === 'Medium') return HIGHLIGHTING_CONFIG.COLORS.PRIORITY.MEDIUM + if (priority === 'Low') return HIGHLIGHTING_CONFIG.COLORS.PRIORITY.LOW + if (priority === 'VeryLow') return HIGHLIGHTING_CONFIG.COLORS.PRIORITY.VERY_LOW + return HIGHLIGHTING_CONFIG.COLORS.PRIORITY.UNKNOWN + } + + const getQueueAnalysisIcon = (analysis?: QueueAnalysis) => { + if (!analysis) return '' + switch (analysis.reason) { + case 'connection_limit': return '🚫' + case 'priority_queue': return '⏳' + case 'resource_contention': return '⚠️' + default: return '❓' + } + } + + const getSizeColor = (bytes?: number) => { + if (!bytes) return HIGHLIGHTING_CONFIG.COLORS.SIZE.DEFAULT + + const sizeKB = bytes / 1024 + + if (sizeKB >= HIGHLIGHTING_CONFIG.SIZE_THRESHOLDS.LARGE) { + return HIGHLIGHTING_CONFIG.COLORS.SIZE.LARGE + } else if (sizeKB >= HIGHLIGHTING_CONFIG.SIZE_THRESHOLDS.MEDIUM) { + return HIGHLIGHTING_CONFIG.COLORS.SIZE.MEDIUM + } else if (sizeKB <= HIGHLIGHTING_CONFIG.SIZE_THRESHOLDS.SMALL) { + return HIGHLIGHTING_CONFIG.COLORS.SIZE.SMALL + } + + return HIGHLIGHTING_CONFIG.COLORS.SIZE.DEFAULT + } + + const getDurationColor = (microseconds?: number) => { + if (!microseconds) return HIGHLIGHTING_CONFIG.COLORS.DURATION.DEFAULT + + const durationMs = microseconds / 1000 + + if (durationMs > HIGHLIGHTING_CONFIG.DURATION_THRESHOLDS.SLOW) { + return HIGHLIGHTING_CONFIG.COLORS.DURATION.SLOW + } else if (durationMs >= HIGHLIGHTING_CONFIG.DURATION_THRESHOLDS.MEDIUM) { + return HIGHLIGHTING_CONFIG.COLORS.DURATION.MEDIUM + } else if (durationMs < HIGHLIGHTING_CONFIG.DURATION_THRESHOLDS.FAST) { + return HIGHLIGHTING_CONFIG.COLORS.DURATION.FAST + } + + return HIGHLIGHTING_CONFIG.COLORS.DURATION.DEFAULT + } + + const getCDNIcon = (analysis?: CDNAnalysis) => { + if (!analysis) return '' + + const providerIcons = { + cloudflare: '🟠', + akamai: '🔵', + aws_cloudfront: '🟡', + fastly: '⚡', + azure_cdn: '🟦', + google_cdn: '🟢', + cdn77: '🟣', + keycdn: '🔶', + unknown: '📡' + } + + const edgeIcon = analysis.isEdge ? '🌐' : '🏠' + const cacheIcon = analysis.cacheStatus === 'hit' ? '✅' : + analysis.cacheStatus === 'miss' ? '❌' : + analysis.cacheStatus === 'expired' ? '⏰' : '' + + return `${providerIcons[analysis.provider]}${edgeIcon}${cacheIcon}` + } + + const getCDNDisplayName = (provider: CDNAnalysis['provider']) => { + const names = { + cloudflare: 'Cloudflare', + akamai: 'Akamai', + aws_cloudfront: 'CloudFront', + fastly: 'Fastly', + azure_cdn: 'Azure CDN', + google_cdn: 'Google CDN', + cdn77: 'CDN77', + keycdn: 'KeyCDN', + unknown: 'Unknown CDN' + } + return names[provider] + } + + const toggleRowExpansion = (requestId: string) => { + const newExpanded = new Set(expandedRows) + if (newExpanded.has(requestId)) { + newExpanded.delete(requestId) + } else { + newExpanded.add(requestId) + } + setExpandedRows(newExpanded) + } + + const truncateUrl = (url: string, maxLength: number = 60) => { + if (url.length <= maxLength) return url + return url.substring(0, maxLength) + '...' + } + + if (loading) { + return ( +
+
+
Loading HTTP requests...
+ +
+ ) + } + + if (error) { + return ( +
+
+

Error Loading Trace Data

+

{error}

+
+
+ ) + } + + return ( +
+

HTTP Requests & Responses

+ + {/* Debug info - remove after investigation */} + {traceData && ( +
+ Debug: Total events: {traceData.traceEvents.length.toLocaleString()}, + Screenshots found: {screenshots.length}, + HTTP requests: {httpRequests.length} +
+ Categories sample: {Array.from(new Set(traceData.traceEvents.slice(0, 1000).map(e => e.cat))).slice(0, 10).join(', ')} +
+ Event names sample: {Array.from(new Set(traceData.traceEvents.slice(0, 1000).map(e => e.name))).slice(0, 10).join(', ')} +
+ )} + + {/* Controls */} +
+ {/* Resource Type Filter */} +
+ + +
+ + {/* Protocol Filter */} +
+ + +
+ + {/* Hostname Filter */} +
+ + +
+ + {/* Priority Filter */} +
+ + +
+ + {/* Search */} +
+ + { + setSearchTerm(e.target.value) + setCurrentPage(1) + }} + style={{ + padding: '8px 12px', + border: '1px solid #ced4da', + borderRadius: '4px', + fontSize: '14px', + minWidth: '300px' + }} + /> +
+ + {/* Queue Analysis Toggle */} +
+ + +
+ + {/* Screenshots Toggle */} + {screenshots.length > 0 && ( +
+ + +
+ )} + + {/* Results count */} +
+ {showScreenshots ? + `${timelineEntries.length.toLocaleString()} timeline entries` : + `${filteredRequests.length.toLocaleString()} requests found`} +
+
+ + {/* Pagination Controls */} + {totalPages > 1 && ( +
+ + + + Page {currentPage} of {totalPages} + + + +
+ )} + + {/* Requests Table */} +
+ + + + + + + + + + + + + + + + + + + + {(showScreenshots ? paginatedTimelineEntries : paginatedRequests.map(req => ({ type: 'request' as const, timestamp: req.timing.start, data: req }))).map((entry, entryIndex) => { + if (entry.type === 'screenshot') { + const screenshot = entry.data as ScreenshotEvent + const firstRequestTime = httpRequests.length > 0 ? httpRequests[0].timing.start : 0 + const timeOffset = screenshot.timestamp - firstRequestTime + + return ( + + + + ) + } + + const request = entry.data as HTTPRequest + const isExpanded = expandedRows.has(request.requestId) + + return ( + <> + toggleRowExpansion(request.requestId)} + > + + + + + + + + + + + + + + + + {/* Expanded Row Details */} + {isExpanded && ( + + + + )} + + ) + })} + +
ExpandMethodStatusTypePriorityStart TimeQueue TimeURLDurationSizeProtocolCDNCache
+
+
+ 📸 Screenshot +
+
+ Time: {formatDuration(timeOffset)} +
+ {`Screenshot { + // Open screenshot in new window for full size viewing + const newWindow = window.open('', '_blank') + if (newWindow) { + newWindow.document.write(` + + Screenshot at ${formatDuration(timeOffset)} + + Full size screenshot + + + `) + } + }} + /> +
+ Click to view full size +
+
+
+ {isExpanded ? '−' : '+'} + + {request.method} + + {request.statusCode || '-'} + + {request.resourceType} + + {request.priority || '-'} + + {formatDuration(request.timing.startOffset)} + HIGHLIGHTING_CONFIG.QUEUE_TIME.HIGH_THRESHOLD ? HIGHLIGHTING_CONFIG.COLORS.STATUS.SERVER_ERROR : '#495057', + fontWeight: request.timing.queueTime && request.timing.queueTime > HIGHLIGHTING_CONFIG.QUEUE_TIME.HIGH_THRESHOLD ? 'bold' : 'normal' + }}> +
+ {formatDuration(request.timing.queueTime)} + {showQueueAnalysis && request.queueAnalysis && ( + + {getQueueAnalysisIcon(request.queueAnalysis)} + + )} +
+
+ + {truncateUrl(request.url)} + + + {formatDuration(request.timing.duration)} + + {formatSize(request.encodedDataLength)} + + {request.protocol || '-'} + + {request.cdnAnalysis ? getCDNIcon(request.cdnAnalysis) : '-'} + + {request.fromCache ? '💾' : request.connectionReused ? '🔄' : '🌐'} +
+
+ + {/* Request Details */} +
+

Request Details

+
+
Request ID: {request.requestId}
+
Method: {request.method}
+
Priority: {request.priority}
+
MIME Type: {request.mimeType || '-'}
+
From Cache: {request.fromCache ? 'Yes' : 'No'}
+
Connection Reused: {request.connectionReused ? 'Yes' : 'No'}
+
+
+ + {/* Network Timing */} +
+

Network Timing

+
+
Start Time: {formatDuration(request.timing.startOffset)}
+
+ Queue Time: {formatDuration(request.timing.queueTime)} + {request.queueAnalysis && ( + + {getQueueAnalysisIcon(request.queueAnalysis)} + + )} +
+
Total Duration: {formatDuration(request.timing.duration)}
+
Network Duration: {formatDuration(request.timing.networkDuration)}
+ {request.timing.dnsStart !== -1 && request.timing.dnsEnd !== -1 && ( +
DNS: {formatDuration((request.timing.dnsEnd || 0) - (request.timing.dnsStart || 0))}
+ )} + {request.timing.connectStart !== -1 && request.timing.connectEnd !== -1 && ( +
Connect: {formatDuration((request.timing.connectEnd || 0) - (request.timing.connectStart || 0))}
+ )} + {request.timing.sslStart !== -1 && request.timing.sslEnd !== -1 && ( +
SSL: {formatDuration((request.timing.sslEnd || 0) - (request.timing.sslStart || 0))}
+ )} + {request.timing.sendStart !== -1 && request.timing.sendEnd !== -1 && ( +
Send: {formatDuration((request.timing.sendEnd || 0) - (request.timing.sendStart || 0))}
+ )} + {request.timing.receiveHeadersStart !== -1 && request.timing.receiveHeadersEnd !== -1 && ( +
Receive Headers: {formatDuration((request.timing.receiveHeadersEnd || 0) - (request.timing.receiveHeadersStart || 0))}
+ )} +
+
+ + {/* Queue Analysis */} + {request.queueAnalysis && request.queueAnalysis.reason !== 'unknown' && ( +
+

+ Queue Analysis {getQueueAnalysisIcon(request.queueAnalysis)} +

+
+
Reason: {request.queueAnalysis.description}
+
Concurrent Requests: {request.queueAnalysis.concurrentRequests}
+ {request.queueAnalysis.relatedRequests && request.queueAnalysis.relatedRequests.length > 0 && ( +
+ Related Request IDs:{' '} + + {request.queueAnalysis.relatedRequests.join(', ')} + {request.queueAnalysis.concurrentRequests > request.queueAnalysis.relatedRequests.length && + ` (+${request.queueAnalysis.concurrentRequests - request.queueAnalysis.relatedRequests.length} more)`} + +
+ )} +
+
+ )} + + {/* CDN Analysis */} + {request.cdnAnalysis && request.cdnAnalysis.provider !== 'unknown' && ( +
+

+ CDN Analysis {getCDNIcon(request.cdnAnalysis)} +

+
+
Provider: {getCDNDisplayName(request.cdnAnalysis.provider)}
+
Source: {request.cdnAnalysis.isEdge ? 'Edge Server' : 'Origin Server'}
+ {request.cdnAnalysis.cacheStatus && request.cdnAnalysis.cacheStatus !== 'unknown' && ( +
Cache Status: {request.cdnAnalysis.cacheStatus.toUpperCase()}
+ )} + {request.cdnAnalysis.edgeLocation && ( +
Edge Location: {request.cdnAnalysis.edgeLocation}
+ )} +
Confidence: {(request.cdnAnalysis.confidence * 100).toFixed(0)}%
+
Detection Method: {request.cdnAnalysis.detectionMethod}
+
+
+ )} + + {/* Response Headers */} + {request.responseHeaders && request.responseHeaders.length > 0 && ( +
+

+ Response Headers ({request.responseHeaders.length}) +

+
+ {request.responseHeaders.map((header, index) => ( +
+
+ {header.name}: +
+
+ {header.value} +
+
+ ))} +
+
+ )} + +
+
+
+ + {(showScreenshots ? paginatedTimelineEntries.length === 0 : paginatedRequests.length === 0) && ( +
+ {showScreenshots ? + 'No timeline entries found matching the current filters' : + 'No HTTP requests found matching the current filters'} +
+ )} +
+ ) +} \ No newline at end of file diff --git a/src/components/PhaseViewer.tsx b/src/components/PhaseViewer.tsx new file mode 100644 index 0000000..c26f114 --- /dev/null +++ b/src/components/PhaseViewer.tsx @@ -0,0 +1,537 @@ +import { useState, useMemo } from 'react' +import { useDatabaseTraceData } from '../hooks/useDatabaseTraceData' +import type { TraceEvent, TraceEventPhase } from '../../types/trace' + +interface ExtendedTraceEvent extends TraceEvent { + dur?: number + tdur?: number + tts?: number +} + +const PHASE_DESCRIPTIONS: Record = { + 'M': 'Metadata', + 'X': 'Complete Events', + 'I': 'Instant Events', + 'B': 'Begin Events', + 'E': 'End Events', + 'D': 'Deprecated', + 'b': 'Nestable Start', + 'e': 'Nestable End', + 'n': 'Nestable Instant', + 'S': 'Async Start', + 'T': 'Async Instant', + 'F': 'Async End', + 'P': 'Sample', + 'C': 'Counter', + 'R': 'Mark' +} + +const ITEMS_PER_PAGE = 50 + +// Helper functions to extract valuable fields +const getURL = (event: ExtendedTraceEvent): string | null => { + const args = event.args as any + return args?.url || args?.beginData?.url || args?.data?.frames?.[0]?.url || null +} + +const getStackTrace = (event: ExtendedTraceEvent): any[] | null => { + const args = event.args as any + return args?.beginData?.stackTrace || null +} + +const getFrameInfo = (event: ExtendedTraceEvent): string | null => { + const args = event.args as any + return args?.beginData?.frame || args?.data?.frameTreeNodeId || null +} + +const getScriptInfo = (event: ExtendedTraceEvent): { contextId?: number, scriptId?: number } => { + const args = event.args as any + return { + contextId: args?.data?.executionContextId, + scriptId: args?.data?.scriptId + } +} + +const getLayoutInfo = (event: ExtendedTraceEvent): { dirtyObjects?: number, totalObjects?: number, elementCount?: number } => { + const args = event.args as any + return { + dirtyObjects: args?.beginData?.dirtyObjects, + totalObjects: args?.beginData?.totalObjects, + elementCount: args?.elementCount + } +} + +const getSampleTraceId = (event: ExtendedTraceEvent): number | null => { + const args = event.args as any + return args?.beginData?.sampleTraceId || null +} + +interface PhaseViewerProps { + traceId: string | null +} + +export default function PhaseViewer({ traceId }: PhaseViewerProps) { + const { traceData, loading, error, stats } = useDatabaseTraceData(traceId) + const [selectedPhase, setSelectedPhase] = useState('all') + const [currentPage, setCurrentPage] = useState(1) + const [searchTerm, setSearchTerm] = useState('') + const [expandedRows, setExpandedRows] = useState>(new Set()) + + const filteredEvents = useMemo(() => { + if (!traceData) return [] + + let events = traceData.traceEvents + + // Filter by phase + if (selectedPhase !== 'all') { + events = events.filter(event => event.ph === selectedPhase) + } + + // Filter by search term + if (searchTerm) { + const term = searchTerm.toLowerCase() + events = events.filter(event => + event.name.toLowerCase().includes(term) || + event.cat.toLowerCase().includes(term) || + event.pid.toString().includes(term) || + event.tid.toString().includes(term) + ) + } + + return events + }, [traceData, selectedPhase, searchTerm]) + + const paginatedEvents = useMemo(() => { + const startIndex = (currentPage - 1) * ITEMS_PER_PAGE + const endIndex = startIndex + ITEMS_PER_PAGE + return filteredEvents.slice(startIndex, endIndex) + }, [filteredEvents, currentPage]) + + const totalPages = Math.ceil(filteredEvents.length / ITEMS_PER_PAGE) + + const formatTimestamp = (ts: number) => { + return (ts / 1000).toFixed(3) + 'ms' + } + + const formatDuration = (dur?: number) => { + if (!dur) return '-' + return (dur / 1000).toFixed(3) + 'ms' + } + + const formatThreadTime = (tts?: number) => { + if (!tts) return '-' + return (tts / 1000).toFixed(3) + 'ms' + } + + const toggleRowExpansion = (eventKey: string) => { + const newExpanded = new Set(expandedRows) + if (newExpanded.has(eventKey)) { + newExpanded.delete(eventKey) + } else { + newExpanded.add(eventKey) + } + setExpandedRows(newExpanded) + } + + const truncateText = (text: string, maxLength: number = 50) => { + if (text.length <= maxLength) return text + return text.substring(0, maxLength) + '...' + } + + if (loading) { + return ( +
+
+
Loading trace events...
+
+ ) + } + + if (error) { + return ( +
+
+

Error Loading Trace Data

+

{error}

+
+
+ ) + } + + if (!traceData || !stats) { + return
No trace data available
+ } + + const phaseOptions = Object.keys(stats.eventsByPhase).sort() + + return ( +
+

Phase Event Viewer

+ + {/* Controls */} +
+ {/* Phase Filter */} +
+ + +
+ + {/* Search */} +
+ + { + setSearchTerm(e.target.value) + setCurrentPage(1) + }} + style={{ + padding: '8px 12px', + border: '1px solid #ced4da', + borderRadius: '4px', + fontSize: '14px', + minWidth: '300px' + }} + /> +
+ + {/* Results count */} +
+ {filteredEvents.length.toLocaleString()} events found +
+
+ + {/* Pagination Controls */} + {totalPages > 1 && ( +
+ + + + Page {currentPage} of {totalPages} + + + +
+ )} + + {/* Events Table */} +
+ + + + + + + + + + + + + + + + {paginatedEvents.map((event, index) => { + const eventKey = `${event.pid}-${event.tid}-${event.ts}-${index}` + const isExpanded = expandedRows.has(eventKey) + const url = getURL(event as ExtendedTraceEvent) + const stackTrace = getStackTrace(event as ExtendedTraceEvent) + const layoutInfo = getLayoutInfo(event as ExtendedTraceEvent) + const scriptInfo = getScriptInfo(event as ExtendedTraceEvent) + const sampleTraceId = getSampleTraceId(event as ExtendedTraceEvent) + + return ( + <> + toggleRowExpansion(eventKey)} + > + + + + + + + + + + + + {/* Expanded Row Details */} + {isExpanded && ( + + + + )} + + ) + })} + +
ExpandPhaseNameCategoryDurationThread TimeURLPIDTID
+ {isExpanded ? '−' : '+'} + + {event.ph} + + {event.name} + + {event.cat} + + {formatDuration((event as ExtendedTraceEvent).dur)} + + {formatThreadTime((event as ExtendedTraceEvent).tts)} + + {url ? ( + + {truncateText(url, 40)} + + ) : '-'} + + {event.pid} + + {event.tid} +
+
+ + {/* Timing Information */} +
+

Timing

+
+
Timestamp: {formatTimestamp(event.ts)}
+
Duration: {formatDuration((event as ExtendedTraceEvent).dur)}
+
Thread Duration: {formatDuration((event as ExtendedTraceEvent).tdur)}
+
Thread Time: {formatThreadTime((event as ExtendedTraceEvent).tts)}
+ {sampleTraceId &&
Sample Trace ID: {sampleTraceId}
} +
+
+ + {/* Script/Context Information */} + {(scriptInfo.contextId || scriptInfo.scriptId) && ( +
+

Script Context

+
+ {scriptInfo.contextId &&
Execution Context: {scriptInfo.contextId}
} + {scriptInfo.scriptId &&
Script ID: {scriptInfo.scriptId}
} +
+
+ )} + + {/* Layout Information */} + {(layoutInfo.dirtyObjects || layoutInfo.totalObjects || layoutInfo.elementCount) && ( +
+

Layout Metrics

+
+ {layoutInfo.dirtyObjects &&
Dirty Objects: {layoutInfo.dirtyObjects}
} + {layoutInfo.totalObjects &&
Total Objects: {layoutInfo.totalObjects}
} + {layoutInfo.elementCount &&
Element Count: {layoutInfo.elementCount}
} +
+
+ )} + + {/* Stack Trace */} + {stackTrace && stackTrace.length > 0 && ( +
+

+ Stack Trace ({stackTrace.length} frames) +

+
+ {stackTrace.slice(0, 10).map((frame: any, frameIndex: number) => ( +
+
+ {frame.functionName || '(anonymous)'} +
+
+ {frame.url ? truncateText(frame.url, 80) : 'unknown'} + {frame.lineNumber && `:${frame.lineNumber}`} + {frame.columnNumber && `:${frame.columnNumber}`} +
+
+ ))} + {stackTrace.length > 10 && ( +
+ ... and {stackTrace.length - 10} more frames +
+ )} +
+
+ )} + +
+
+
+ + {paginatedEvents.length === 0 && ( +
+ No events found matching the current filters +
+ )} + + +
+ ) +} + +function getPhaseColor(phase: TraceEventPhase): string { + const colors: Record = { + 'M': '#6c757d', // Metadata - gray + 'X': '#007bff', // Complete - blue + 'I': '#28a745', // Instant - green + 'B': '#ffc107', // Begin - yellow + 'E': '#fd7e14', // End - orange + 'D': '#6f42c1', // Deprecated - purple + 'b': '#20c997', // Nestable start - teal + 'e': '#e83e8c', // Nestable end - pink + 'n': '#17a2b8', // Nestable instant - cyan + 'S': '#dc3545', // Async start - red + 'T': '#ffc107', // Async instant - warning + 'F': '#fd7e14', // Async end - orange + 'P': '#6f42c1', // Sample - purple + 'C': '#20c997', // Counter - teal + 'R': '#e83e8c' // Mark - pink + } + return colors[phase] || '#6c757d' +} \ No newline at end of file diff --git a/src/components/TraceSelector.tsx b/src/components/TraceSelector.tsx new file mode 100644 index 0000000..543e524 --- /dev/null +++ b/src/components/TraceSelector.tsx @@ -0,0 +1,331 @@ +import { useState, useEffect } from 'react' +import { traceDatabase, type TraceRecord } from '../utils/traceDatabase' + +interface TraceSelectorProps { + onTraceSelect: (traceId: string) => void + onUploadNew: () => void +} + +export default function TraceSelector({ onTraceSelect, onUploadNew }: TraceSelectorProps) { + const [traces, setTraces] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [editingId, setEditingId] = useState(null) + const [editingName, setEditingName] = useState('') + + useEffect(() => { + loadTraces() + }, []) + + const loadTraces = async () => { + try { + setLoading(true) + const allTraces = await traceDatabase.getAllTraces() + // Sort by upload date (newest first) + allTraces.sort((a, b) => b.uploadedAt.getTime() - a.uploadedAt.getTime()) + setTraces(allTraces) + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to load traces') + } finally { + setLoading(false) + } + } + + const handleDelete = async (id: string) => { + if (confirm('Are you sure you want to delete this trace? This action cannot be undone.')) { + try { + await traceDatabase.deleteTrace(id) + setTraces(traces.filter(t => t.id !== id)) + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to delete trace') + } + } + } + + const handleRename = async (id: string, newName: string) => { + if (!newName.trim()) return + + try { + await traceDatabase.updateTraceName(id, newName.trim()) + setTraces(traces.map(t => t.id === id ? { ...t, name: newName.trim() } : t)) + setEditingId(null) + setEditingName('') + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to rename trace') + } + } + + const startEdit = (trace: TraceRecord) => { + setEditingId(trace.id) + setEditingName(trace.name) + } + + const cancelEdit = () => { + setEditingId(null) + setEditingName('') + } + + const formatFileSize = (bytes: number) => { + if (bytes < 1024) return `${bytes} B` + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` + return `${(bytes / (1024 * 1024)).toFixed(1)} MB` + } + + const formatDuration = (microseconds: number) => { + const seconds = microseconds / 1000000 + if (seconds < 60) return `${seconds.toFixed(1)}s` + const minutes = seconds / 60 + return `${minutes.toFixed(1)}m` + } + + const formatDate = (date: Date) => { + return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit' + }) + } + + if (loading) { + return ( +
+
Loading traces...
+
+ ) + } + + return ( +
+
+

+ Performance Traces ({traces.length}) +

+ + +
+ + {error && ( +
+ {error} +
+ )} + + {traces.length === 0 ? ( +
+
📊
+

+ No traces found +

+

+ Upload your first Chrome DevTools Performance trace to get started. +

+ +
+ ) : ( +
+ {traces.map((trace) => ( +
{ + e.currentTarget.style.borderColor = '#007bff' + e.currentTarget.style.boxShadow = '0 4px 8px rgba(0,123,255,0.15)' + }} + onMouseLeave={(e) => { + e.currentTarget.style.borderColor = '#dee2e6' + e.currentTarget.style.boxShadow = '0 2px 4px rgba(0,0,0,0.05)' + }} + onClick={() => onTraceSelect(trace.id)} + > +
+ {editingId === trace.id ? ( + setEditingName(e.target.value)} + onBlur={() => handleRename(trace.id, editingName)} + onKeyDown={(e) => { + if (e.key === 'Enter') handleRename(trace.id, editingName) + if (e.key === 'Escape') cancelEdit() + e.stopPropagation() + }} + onClick={(e) => e.stopPropagation()} + autoFocus + style={{ + fontSize: '16px', + fontWeight: '600', + border: '1px solid #007bff', + borderRadius: '4px', + padding: '4px 8px', + background: 'white', + outline: 'none' + }} + /> + ) : ( +

+ {trace.name} +

+ )} + +
+ + +
+
+ +
+
Events: {trace.eventCount.toLocaleString()}
+
Size: {formatFileSize(trace.fileSize)}
+
Duration: {formatDuration(trace.duration)}
+
Uploaded: {formatDate(trace.uploadedAt)}
+
+ +
+ Click to analyze → +
+
+ ))} +
+ )} +
+ ) +} \ No newline at end of file diff --git a/src/components/TraceStats.tsx b/src/components/TraceStats.tsx new file mode 100644 index 0000000..33feb76 --- /dev/null +++ b/src/components/TraceStats.tsx @@ -0,0 +1,165 @@ +import { getTraceStats } from '../utils/traceLoader' + +interface TraceStatsProps { + stats: ReturnType +} + +export default function TraceStats({ stats }: TraceStatsProps) { + const formatNumber = (num: number) => { + return new Intl.NumberFormat().format(num) + } + + const formatDuration = (microseconds: number) => { + const milliseconds = microseconds / 1000 + if (milliseconds < 1000) { + return `${milliseconds.toFixed(2)}ms` + } + const seconds = milliseconds / 1000 + return `${seconds.toFixed(2)}s` + } + + const formatTimestamp = (timestamp: number) => { + return new Date(timestamp / 1000).toLocaleString() + } + + const getPhaseDescription = (phase: string) => { + const descriptions: Record = { + 'M': 'Metadata', + 'X': 'Complete Events', + 'I': 'Instant Events', + 'B': 'Begin Events', + 'E': 'End Events', + 'D': 'Deprecated', + 'b': 'Nestable Start', + 'e': 'Nestable End', + 'n': 'Nestable Instant', + 'S': 'Async Start', + 'T': 'Async Instant', + 'F': 'Async End', + 'P': 'Sample', + 'C': 'Counter', + 'R': 'Mark' + } + return descriptions[phase] || `Unknown (${phase})` + } + + return ( +
+

Trace Statistics

+ +
+ {/* Overview Section */} +
+

Overview

+
+
Total Events: {formatNumber(stats.totalEvents)}
+
Processes: {stats.processCount}
+
Threads: {stats.threadCount}
+
Duration: {formatDuration(stats.timeRange.duration)}
+
+
+ + {/* Metadata Section */} +
+

Metadata

+
+
Source: {stats.metadata.source}
+
Hardware Threads: {stats.metadata.hardwareConcurrency}
+
Start Time: {new Date(stats.metadata.startTime).toLocaleString()}
+
+
+ + {/* Time Range Section */} +
+

Time Range

+
+
Start: {formatTimestamp(stats.timeRange.start)}
+
End: {formatTimestamp(stats.timeRange.end)}
+
Duration: {formatDuration(stats.timeRange.duration)}
+
+
+
+ + {/* Event Types Section */} +
+

Events by Type

+
+ {Object.entries(stats.eventsByPhase) + .sort(([, a], [, b]) => b - a) + .map(([phase, count]) => ( +
+
+ {getPhaseDescription(phase)} +
+
+ Phase: {phase} +
+
+ {formatNumber(count)} +
+
+ {((count / stats.totalEvents) * 100).toFixed(1)}% +
+
+ ))} +
+
+
+ ) +} \ No newline at end of file diff --git a/src/components/TraceUpload.tsx b/src/components/TraceUpload.tsx new file mode 100644 index 0000000..a56ca5a --- /dev/null +++ b/src/components/TraceUpload.tsx @@ -0,0 +1,247 @@ +import { useState } from 'react' +import type { TraceFile } from '../../types/trace' +import { traceDatabase } from '../utils/traceDatabase' + +interface TraceUploadProps { + onUploadSuccess: (traceId: string) => void +} + +export default function TraceUpload({ onUploadSuccess }: TraceUploadProps) { + const [uploading, setUploading] = useState(false) + const [dragOver, setDragOver] = useState(false) + const [error, setError] = useState(null) + + const validateTraceFile = (data: any): data is TraceFile => { + return data && + data.metadata && + Array.isArray(data.traceEvents) && + data.traceEvents.length > 0 + } + + const processFile = async (file: File) => { + setUploading(true) + setError(null) + + try { + // Validate file type + if (!file.name.toLowerCase().endsWith('.json')) { + throw new Error('Please upload a JSON trace file') + } + + // Validate file size (max 200MB) + if (file.size > 200 * 1024 * 1024) { + throw new Error('Trace file too large (max 200MB)') + } + + // Read file content + const text = await file.text() + + // Parse JSON + let traceData: TraceFile + try { + traceData = JSON.parse(text) + } catch (parseError) { + throw new Error('Invalid JSON format') + } + + // Validate trace structure + if (!validateTraceFile(traceData)) { + throw new Error('Invalid trace file format. Expected Chrome DevTools Performance trace.') + } + + // Store in database + const traceId = await traceDatabase.addTrace(traceData, file.name, file.size) + + onUploadSuccess(traceId) + } catch (err) { + setError(err instanceof Error ? err.message : 'Upload failed') + } finally { + setUploading(false) + } + } + + const handleFileSelect = (event: React.ChangeEvent) => { + const file = event.target.files?.[0] + if (file) { + processFile(file) + } + } + + const handleDrop = (event: React.DragEvent) => { + event.preventDefault() + setDragOver(false) + + const file = event.dataTransfer.files[0] + if (file) { + processFile(file) + } + } + + const handleDragOver = (event: React.DragEvent) => { + event.preventDefault() + setDragOver(true) + } + + const handleDragLeave = (event: React.DragEvent) => { + event.preventDefault() + setDragOver(false) + } + + return ( +
+
+

+ Performance Trace Analyzer +

+

+ Upload Chrome DevTools Performance trace files to analyze network requests, + queue times, CDN usage, and performance bottlenecks. +

+
+ +
document.getElementById('trace-upload')?.click()} + > + {uploading ? ( +
+
+
+ Processing trace file... +
+
+ ) : ( +
+
📊
+
+ Drop your trace file here +
+
+ or click to browse files +
+
+ Supports JSON trace files up to 200MB +
+
+ )} + + +
+ + {error && ( +
+ {error} +
+ )} + +
+

+ How to capture a trace: +

+
    +
  1. Open Chrome DevTools (F12)
  2. +
  3. Go to the Performance tab
  4. +
  5. Click the record button and reload your page
  6. +
  7. Stop recording and click "Save profile"
  8. +
  9. Upload the saved .json file here
  10. +
+
+ + +
+ ) +} \ No newline at end of file diff --git a/src/components/TraceViewer.tsx b/src/components/TraceViewer.tsx new file mode 100644 index 0000000..48a1ace --- /dev/null +++ b/src/components/TraceViewer.tsx @@ -0,0 +1,128 @@ +import { useDatabaseTraceData } from '../hooks/useDatabaseTraceData' +import TraceStats from './TraceStats' + +interface TraceViewerProps { + traceId: string | null +} + +export default function TraceViewer({ traceId }: TraceViewerProps) { + const { traceData, loading, error, stats, reload } = useDatabaseTraceData(traceId) + + if (loading) { + return ( +
+
+
Loading trace data...
+
+ This may take a moment for large trace files (90.9MB) +
+ +
+ ) + } + + if (error) { + return ( +
+
+

Error Loading Trace Data

+

{error}

+
+ +
+ ) + } + + if (!traceData || !stats) { + return ( +
+ No trace data available +
+ ) + } + + return ( +
+
+

Performance Trace Viewer

+ +
+ + +
+ ) +} \ No newline at end of file diff --git a/src/hooks/useDatabaseTraceData.ts b/src/hooks/useDatabaseTraceData.ts new file mode 100644 index 0000000..1f23011 --- /dev/null +++ b/src/hooks/useDatabaseTraceData.ts @@ -0,0 +1,79 @@ +import { useState, useEffect } from 'react' +import type { TraceFile } from '../../types/trace' +import { traceDatabase } from '../utils/traceDatabase' +import { getTraceStats } from '../utils/traceLoader' + +interface UseDatabaseTraceDataResult { + traceData: TraceFile | null + loading: boolean + error: string | null + stats: ReturnType | null + reload: () => void +} + +/** + * React hook for loading and managing trace data from IndexedDB + * @param traceId - The ID of the trace to load, or null to load none + * @returns Object containing trace data, loading state, error, and statistics + */ +export function useDatabaseTraceData(traceId: string | null): UseDatabaseTraceDataResult { + const [traceData, setTraceData] = useState(null) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const [stats, setStats] = useState | null>(null) + + const loadData = async () => { + if (!traceId) { + setTraceData(null) + setStats(null) + setError(null) + setLoading(false) + return + } + + try { + setLoading(true) + setError(null) + + // Initialize database if needed + await traceDatabase.init() + + // Load trace from database + const traceRecord = await traceDatabase.getTrace(traceId) + + if (!traceRecord) { + throw new Error('Trace not found in database') + } + + setTraceData(traceRecord.traceData) + + // Calculate statistics + const traceStats = getTraceStats(traceRecord.traceData) + setStats(traceStats) + + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred' + setError(errorMessage) + setTraceData(null) + setStats(null) + } finally { + setLoading(false) + } + } + + const reload = () => { + loadData() + } + + useEffect(() => { + loadData() + }, [traceId]) + + return { + traceData, + loading, + error, + stats, + reload + } +} \ No newline at end of file diff --git a/src/hooks/useTraceData.ts b/src/hooks/useTraceData.ts new file mode 100644 index 0000000..4af02d5 --- /dev/null +++ b/src/hooks/useTraceData.ts @@ -0,0 +1,60 @@ +import { useState, useEffect } from 'react' +import type { TraceFile } from '../../types/trace' +import { loadTraceFile, getTraceStats } from '../utils/traceLoader' + +interface UseTraceDataResult { + traceData: TraceFile | null + loading: boolean + error: string | null + stats: ReturnType | null + reload: () => void +} + +/** + * React hook for loading and managing trace data + * @returns Object containing trace data, loading state, error, and statistics + */ +export function useTraceData(): UseTraceDataResult { + const [traceData, setTraceData] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [stats, setStats] = useState | null>(null) + + const loadData = async () => { + try { + setLoading(true) + setError(null) + + const data = await loadTraceFile() + setTraceData(data) + + // Calculate statistics + const traceStats = getTraceStats(data) + setStats(traceStats) + + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred' + setError(errorMessage) + setTraceData(null) + setStats(null) + } finally { + setLoading(false) + } + } + + const reload = () => { + loadData() + } + + useEffect(() => { + loadData() + }, []) + + return { + traceData, + loading, + error, + stats, + reload + } +} \ No newline at end of file diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/index.css @@ -0,0 +1 @@ + diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..bef5202 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/src/utils/traceDatabase.ts b/src/utils/traceDatabase.ts new file mode 100644 index 0000000..07f7f96 --- /dev/null +++ b/src/utils/traceDatabase.ts @@ -0,0 +1,159 @@ +import type { TraceFile } from '../../types/trace' + +export interface TraceRecord { + id: string + name: string + uploadedAt: Date + fileSize: number + eventCount: number + duration: number + traceData: TraceFile +} + +const DB_NAME = 'PerfVizTraces' +const DB_VERSION = 1 +const STORE_NAME = 'traces' + +class TraceDatabase { + private db: IDBDatabase | null = null + + async init(): Promise { + return new Promise((resolve, reject) => { + const request = indexedDB.open(DB_NAME, DB_VERSION) + + request.onerror = () => reject(request.error) + request.onsuccess = () => { + this.db = request.result + resolve() + } + + request.onupgradeneeded = (event) => { + const db = (event.target as IDBOpenDBRequest).result + + // Create traces object store + const store = db.createObjectStore(STORE_NAME, { keyPath: 'id' }) + store.createIndex('name', 'name', { unique: false }) + store.createIndex('uploadedAt', 'uploadedAt', { unique: false }) + store.createIndex('fileSize', 'fileSize', { unique: false }) + } + }) + } + + async addTrace(traceData: TraceFile, filename: string, fileSize: number): Promise { + if (!this.db) throw new Error('Database not initialized') + + const id = `trace_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` + + // Calculate basic stats + const eventCount = traceData.traceEvents.length + let minTs = Infinity + let maxTs = -Infinity + + for (const event of traceData.traceEvents) { + if (event.ts > 0) { + minTs = Math.min(minTs, event.ts) + maxTs = Math.max(maxTs, event.ts) + } + } + + const duration = maxTs - minTs + + const record: TraceRecord = { + id, + name: filename, + uploadedAt: new Date(), + fileSize, + eventCount, + duration, + traceData + } + + return new Promise((resolve, reject) => { + const transaction = this.db!.transaction([STORE_NAME], 'readwrite') + const store = transaction.objectStore(STORE_NAME) + const request = store.add(record) + + request.onerror = () => reject(request.error) + request.onsuccess = () => resolve(id) + }) + } + + async getAllTraces(): Promise { + if (!this.db) throw new Error('Database not initialized') + + return new Promise((resolve, reject) => { + const transaction = this.db!.transaction([STORE_NAME], 'readonly') + const store = transaction.objectStore(STORE_NAME) + const request = store.getAll() + + request.onerror = () => reject(request.error) + request.onsuccess = () => resolve(request.result) + }) + } + + async getTrace(id: string): Promise { + if (!this.db) throw new Error('Database not initialized') + + return new Promise((resolve, reject) => { + const transaction = this.db!.transaction([STORE_NAME], 'readonly') + const store = transaction.objectStore(STORE_NAME) + const request = store.get(id) + + request.onerror = () => reject(request.error) + request.onsuccess = () => resolve(request.result || null) + }) + } + + async deleteTrace(id: string): Promise { + if (!this.db) throw new Error('Database not initialized') + + return new Promise((resolve, reject) => { + const transaction = this.db!.transaction([STORE_NAME], 'readwrite') + const store = transaction.objectStore(STORE_NAME) + const request = store.delete(id) + + request.onerror = () => reject(request.error) + request.onsuccess = () => resolve() + }) + } + + async updateTraceName(id: string, newName: string): Promise { + if (!this.db) throw new Error('Database not initialized') + + return new Promise((resolve, reject) => { + const transaction = this.db!.transaction([STORE_NAME], 'readwrite') + const store = transaction.objectStore(STORE_NAME) + const getRequest = store.get(id) + + getRequest.onsuccess = () => { + const record = getRequest.result + if (record) { + record.name = newName + const updateRequest = store.put(record) + updateRequest.onsuccess = () => resolve() + updateRequest.onerror = () => reject(updateRequest.error) + } else { + reject(new Error('Trace not found')) + } + } + + getRequest.onerror = () => reject(getRequest.error) + }) + } + + async clearAllTraces(): Promise { + if (!this.db) throw new Error('Database not initialized') + + return new Promise((resolve, reject) => { + const transaction = this.db!.transaction([STORE_NAME], 'readwrite') + const store = transaction.objectStore(STORE_NAME) + const request = store.clear() + + request.onerror = () => reject(request.error) + request.onsuccess = () => resolve() + }) + } +} + +// Singleton instance +export const traceDatabase = new TraceDatabase() \ No newline at end of file diff --git a/src/utils/traceLoader.ts b/src/utils/traceLoader.ts new file mode 100644 index 0000000..031ff20 --- /dev/null +++ b/src/utils/traceLoader.ts @@ -0,0 +1,136 @@ +import type { TraceFile } from '../../types/trace' + +/** + * Loads and parses the trace.json file from the public directory with streaming support + * @returns Promise that resolves to the parsed trace data + */ +export async function loadTraceFile(): Promise { + try { + const response = await fetch('/trace.json') + + if (!response.ok) { + throw new Error(`Failed to load trace.json: ${response.status} ${response.statusText}`) + } + + // Get the response as text first to avoid JSON.parse call stack issues + const textData = await response.text() + + // Parse JSON with error handling for large files + let traceData: TraceFile + try { + // Use a timeout to prevent blocking the main thread too long + traceData = await parseJSONSafely(textData) + } catch (parseError) { + throw new Error(`Failed to parse trace JSON: ${parseError}`) + } + + // Basic validation to ensure the structure matches our expected format + if (!traceData.metadata || !Array.isArray(traceData.traceEvents)) { + throw new Error('Invalid trace file format: missing metadata or traceEvents array') + } + + return traceData + } catch (error) { + console.error('Error loading trace file:', error) + throw error + } +} + +/** + * Safely parse JSON with chunked processing to avoid call stack issues + */ +async function parseJSONSafely(jsonString: string): Promise { + return new Promise((resolve, reject) => { + // Use setTimeout to prevent blocking the main thread + setTimeout(() => { + try { + const parsed = JSON.parse(jsonString) + resolve(parsed) + } catch (error) { + reject(error) + } + }, 0) + }) +} + +/** + * Loads trace file with error handling and loading state management + * Useful for React components that need loading states + */ +export async function loadTraceFileWithStatus(): Promise<{ + data: TraceFile | null + loading: boolean + error: string | null +}> { + try { + const data = await loadTraceFile() + return { + data, + loading: false, + error: null + } + } catch (error) { + return { + data: null, + loading: false, + error: error instanceof Error ? error.message : 'Unknown error occurred' + } + } +} + +/** + * Utility function to get basic trace statistics with optimized processing for large datasets + * @param traceData - The loaded trace data + * @returns Object containing basic statistics about the trace + */ +export function getTraceStats(traceData: TraceFile) { + const { traceEvents, metadata } = traceData + + // Initialize counters + const eventsByPhase: Record = {} + const processes = new Set() + const threads = new Set() + let minTimestamp = Infinity + let maxTimestamp = -Infinity + + // Single pass through events to calculate all stats efficiently + for (const event of traceEvents) { + // Count phase types + eventsByPhase[event.ph] = (eventsByPhase[event.ph] || 0) + 1 + + // Track processes and threads + processes.add(event.pid) + threads.add(`${event.pid}:${event.tid}`) + + // Track time range (only for events with valid timestamps) + if (event.ts > 0) { + minTimestamp = Math.min(minTimestamp, event.ts) + maxTimestamp = Math.max(maxTimestamp, event.ts) + } + } + + // Handle case where no valid timestamps were found + if (minTimestamp === Infinity) { + minTimestamp = 0 + maxTimestamp = 0 + } + + const duration = maxTimestamp - minTimestamp + + return { + totalEvents: traceEvents.length, + eventsByPhase, + timeRange: { + start: minTimestamp, + end: maxTimestamp, + duration: duration + }, + processCount: processes.size, + threadCount: threads.size, + metadata: { + source: metadata.source, + startTime: metadata.startTime, + hardwareConcurrency: metadata.hardwareConcurrency + } + } +} \ No newline at end of file diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..227a6c6 --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..f85a399 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/types/trace.ts b/types/trace.ts new file mode 100644 index 0000000..d1ac991 --- /dev/null +++ b/types/trace.ts @@ -0,0 +1,161 @@ +/** + * Chrome DevTools Performance Trace Format + * Based on the Trace Event Format specification + */ + +// Main trace file structure +export interface TraceFile { + metadata: TraceMetadata + traceEvents: TraceEvent[] +} + +// Metadata section +export interface TraceMetadata { + source: string + startTime: string + hardwareConcurrency: number + dataOrigin: string + modifications: TraceModifications +} + +export interface TraceModifications { + entriesModifications: { + hiddenEntries: unknown[] + expandableEntries: unknown[] + } + initialBreadcrumb: { + window: { + min: number + max: number + range: number + } + child: null | unknown + } + annotations: { + entryLabels: unknown[] + labelledTimeRanges: unknown[] + linksBetweenEntries: unknown[] + } +} + +// Base trace event structure +export interface BaseTraceEvent { + /** Event arguments - varies by event type */ + args: Record + /** Category */ + cat: string + /** Event name */ + name: string + /** Phase type */ + ph: TraceEventPhase + /** Process ID */ + pid: number + /** Thread ID */ + tid: number + /** Timestamp in microseconds */ + ts: number + /** Thread timestamp in microseconds (optional) */ + tts?: number +} + +// Specific event types +export interface MetadataEvent extends BaseTraceEvent { + ph: 'M' + args: { + name?: string + uptime?: string + [key: string]: unknown + } +} + +export interface DurationEvent extends BaseTraceEvent { + ph: 'X' + /** Duration in microseconds */ + dur: number + /** Thread duration in microseconds (optional) */ + tdur?: number +} + +export interface InstantEvent extends BaseTraceEvent { + ph: 'I' + /** Scope (optional) */ + s?: 't' | 'p' | 'g' + /** Layer tree ID (optional) */ + layerTreeId?: number + /** Layer ID (optional) */ + layerId?: number +} + +export interface BeginEvent extends BaseTraceEvent { + ph: 'B' +} + +export interface EndEvent extends BaseTraceEvent { + ph: 'E' +} + +// Union type for all trace events +export type TraceEvent = MetadataEvent | DurationEvent | InstantEvent | BeginEvent | EndEvent + +// Phase type enumeration +export type TraceEventPhase = + | 'M' // Metadata + | 'X' // Complete (duration) + | 'I' // Instant + | 'B' // Begin + | 'E' // End + | 'D' // Deprecated + | 'b' // Nestable start + | 'e' // Nestable end + | 'n' // Nestable instant + | 'S' // Async start + | 'T' // Async instant + | 'F' // Async end + | 'P' // Sample + | 'C' // Counter + | 'R' // Mark + +// Common argument types found in trace events +export interface TracingStartedArgs { + data: { + frameTreeNodeId: number + frames: Array<{ + frame: string + isInPrimaryMainFrame: boolean + isOutermostMainFrame: boolean + name: string + processId: number + url: string + }> + persistentIds: boolean + } +} + +export interface UpdateLayerArgs { + layerId: number + layerTreeId: number +} + +export interface NeedsBeginFrameChangedArgs { + data: { + needsBeginFrame: number + } + layerTreeId: number +} + +export interface RunTaskArgs { + [key: string]: unknown +} + +// Thread and process name arguments +export interface ThreadNameArgs { + name: string +} + +export interface ProcessNameArgs { + name: string +} + +export interface ProcessUptimeArgs { + uptime: string +} \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..8b0f57b --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +})