diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..84e85af --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,46 @@ +## ๐Ÿ“ Type of Change + +Select the type of change that best describes this PR: + +- [ ] ๐Ÿš€ **Feature** (non-breaking change which adds new functionality) +- [ ] ๐Ÿ› **Bug Fix** (non-breaking change which fixes an issue) +- [ ] โšก **Optimization / Refactoring** (code structure or performance improvement) +- [ ] ๐Ÿ“– **Documentation** (README, CHANGELOG, inline JSDoc updates) +- [ ] ๐Ÿ“ฆ **Release** (version bump and release preparation) +- [ ] โš™๏ธ **CI/CD & Tooling** (GitHub Actions, build config, scripts) + +--- + +## ๐Ÿ“Œ Description & Motivation + +Provide a concise summary of the changes introduced in this PR and the problem/user story it resolves. + +--- + +## โœจ Key Changes & Highlights + +- Feature/fix item 1 +- Feature/fix item 2 +- Feature/fix item 3 + +--- + +## ๐Ÿ“ธ Screenshots / Demos (If Applicable) + +| Feature | Preview | +| :------------ | :-----------------------------------------: | +| **Feature 1** | | +| **Feature 2** | | + +--- + +## ๐Ÿงช Quality & Verification Checklist + +Before submitting this PR, please check and verify the following: + +- [ ] My code follows the project's code style and guidelines. +- [ ] I have executed `pnpm check-types` and verified **0 TypeScript errors**. +- [ ] I have executed `pnpm test` and all unit tests pass **100%**. +- [ ] I have executed `pnpm --filter mobile-devtools build` and the package builds cleanly. +- [ ] I have executed `pnpm --filter web lint` and verified **0 ESLint warnings/errors**. +- [ ] I have updated relevant documentation ([`README.md`](./README.md), [`CHANGELOG.md`](./packages/mobile-devtools/CHANGELOG.md), and inline TSDoc comments) if applicable. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2c39729 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,42 @@ +name: CI & Quality Checks + +on: + push: + branches: [master, main] + pull_request: + branches: [master, main] + +jobs: + ci: + name: Typecheck, Build & Test + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 9.15.0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build mobile-devtools package + run: pnpm --filter mobile-devtools build + + - name: Check TypeScript Types + run: pnpm check-types + + - name: Run Unit Tests + run: pnpm test + + - name: Run Web Application Linting + run: pnpm --filter web lint diff --git a/README.md b/README.md index 1fbf5fc..3288bb4 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ [![Turborepo](https://img.shields.io/badge/Monorepo-Turborepo-ef4444.svg)](https://turbo.build/) [![React](https://img.shields.io/badge/Adapter-React_18%2F19-61dafb.svg)](https://react.dev/) [![Vue](https://img.shields.io/badge/Adapter-Vue_3-42b883.svg)](https://vuejs.org/) +[![Svelte](https://img.shields.io/badge/Adapter-Svelte_4%2F5-ff3e00.svg)](https://svelte.dev/) [![Vanilla JS](https://img.shields.io/badge/Adapter-Vanilla_JS-f7df1e.svg)](https://developer.mozilla.org/en-US/docs/Web/JavaScript) --- @@ -22,6 +23,7 @@ - [๐Ÿš€ Framework Quickstart](#-framework-quickstart) - [โš›๏ธ React Integration](#%EF%B8%8F-react-integration) - [๐Ÿ’š Vue 3 Integration](#-vue-3-integration) + - [๐Ÿงก Svelte 4/5 Integration](#-svelte-45-integration) - [๐Ÿฆ Vanilla JS / Legacy Apps](#-vanilla-js--legacy-apps) - [โš™๏ธ Full Configuration & Props Reference](#%EF%B8%8F-full-configuration--props-reference) - [๐ŸŽจ Theme Engine & Customization](#-theme-engine--customization) @@ -34,13 +36,13 @@ ## ๐Ÿ“ธ Showcase

- Console Tab - Elements Tab - Network Tab + Console Tab + Elements Tab + Network Tab

- Storage Tab - System Tab + Storage Tab + System Tab

--- @@ -74,28 +76,28 @@ Debugging mobile web applications or QA staging builds on physical smartphones, - ๐ŸŽจ **Granular UI Style Overrides (`styles`)**: Fine-grained inline CSS style overrides for badge, drawer, overlay, and handle (`styles={{ badge: {}, drawer: {}, overlay: {} }}`). - ๐ŸŽจ **Dynamic Theme Engine**: Built-in Light Mode and Dark Mode with auto-contrast luminance detection, accent color swatches, and custom background palettes. - ๐Ÿงช **Comprehensive Test Suite**: Tested with **65 Unit Tests (100% Passed)** + **21 Playwright E2E Tests (100% Passed)** across Desktop Chrome, Mobile Chrome, and Mobile Safari. -- ๐Ÿงฉ **Framework Agnostic**: Native support for **React 18/19**, **Vue 3**, and **Vanilla JS**. +- ๐Ÿงฉ **Framework Agnostic**: Native support for **React 18/19**, **Vue 3**, **Svelte 4/5**, and **Vanilla JS**. --- ## ๐Ÿ—๏ธ Technical Architecture ```text -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ mobile-devtools โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ”‚ - โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” - โ–ผ โ–ผ โ–ผ - Vanilla JS React Vue 3 - (mobile-devtools) (mobile-devtools/react) (mobile-devtools/vue) - โ”‚ โ”‚ โ”‚ - โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ”‚ - โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” - โ”‚ Native Shadow DOM Host โ”‚ - โ”‚ โ”‚ - โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ mobile-devtools โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ–ผ โ–ผ โ–ผ โ–ผ + Vanilla JS React Vue 3 Svelte +(mobile-devtools) (mobile-devtools/react) (mobile-devtools/vue) (mobile-devtools/svelte) + โ”‚ โ”‚ โ”‚ โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ Native Shadow DOM Host โ”‚ + โ”‚ โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ @@ -197,6 +199,45 @@ Import from `mobile-devtools/vue`: --- +### ๐Ÿ”ฅ Svelte Integration + +Import from `mobile-devtools/svelte`: + +```svelte + + +
+ +
+``` + +### ๐Ÿงก Svelte 4/5 Integration + +Import from `mobile-devtools/svelte`: + +```svelte + + +
+ +
+``` + +--- + ### ๐Ÿฆ Vanilla JS / Legacy Apps Import directly from `mobile-devtools`: diff --git a/apps/web/eslint.config.js b/apps/web/eslint.config.js index e0f8067..bef3d7e 100644 --- a/apps/web/eslint.config.js +++ b/apps/web/eslint.config.js @@ -1,4 +1,105 @@ -import { config as reactJsConfig } from "@mobile-devtools/eslint-config/react-internal"; +import boundaries from 'eslint-plugin-boundaries'; +import { config as reactJsConfig } from '@mobile-devtools/eslint-config/react-internal'; /** @type {import("eslint").Linter.Config[]} */ -export default reactJsConfig; +export default [ + ...reactJsConfig, + { + plugins: { + boundaries, + }, + settings: { + 'import/resolver': { + typescript: { + alwaysTryTypes: true, + }, + }, + 'boundaries/elements': [ + { + type: 'app', + pattern: ['src/app/**/*'], + }, + { + type: 'views', + pattern: ['src/views/**/*'], + }, + { + type: 'widgets', + pattern: ['src/widgets/**/*'], + }, + { + type: 'features', + pattern: ['src/features/**/*'], + }, + { + type: 'entities', + pattern: ['src/entities/**/*'], + }, + { + type: 'shared', + pattern: ['src/shared/**/*'], + }, + ], + }, + rules: { + 'boundaries/dependencies': [ + 'error', + { + default: 'disallow', + message: '{{file.type}} is not allowed to import {{dependency.type}}', + policies: [ + { + from: { element: { type: 'app' } }, + allow: [ + { to: { element: { type: 'views' } } }, + { to: { element: { type: 'widgets' } } }, + { to: { element: { type: 'features' } } }, + { to: { element: { type: 'entities' } } }, + { to: { element: { type: 'shared' } } }, + { to: { element: { type: 'app' } } }, + ], + }, + { + from: { element: { type: 'views' } }, + allow: [ + { to: { element: { type: 'widgets' } } }, + { to: { element: { type: 'features' } } }, + { to: { element: { type: 'entities' } } }, + { to: { element: { type: 'shared' } } }, + { to: { element: { type: 'views' } } }, + ], + }, + { + from: { element: { type: 'widgets' } }, + allow: [ + { to: { element: { type: 'features' } } }, + { to: { element: { type: 'entities' } } }, + { to: { element: { type: 'shared' } } }, + { to: { element: { type: 'widgets' } } }, + ], + }, + { + from: { element: { type: 'features' } }, + allow: [ + { to: { element: { type: 'entities' } } }, + { to: { element: { type: 'shared' } } }, + { to: { element: { type: 'features' } } }, + ], + }, + { + from: { element: { type: 'entities' } }, + allow: [ + { to: { element: { type: 'shared' } } }, + { to: { element: { type: 'entities' } } }, + ], + }, + { + from: { element: { type: 'shared' } }, + allow: [{ to: { element: { type: 'shared' } } }], + }, + ], + }, + ], + }, + }, +]; diff --git a/apps/web/package.json b/apps/web/package.json index 6235509..497bec3 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -38,6 +38,8 @@ "@types/react-dom": "catalog:", "@vitejs/plugin-react": "catalog:", "eslint": "^9.39.1", + "eslint-import-resolver-typescript": "^4.4.5", + "eslint-plugin-boundaries": "^7.2.0", "postcss": "^8.5.26", "rimraf": "^6.1.3", "tailwindcss": "^4.3.3", diff --git a/apps/web/playwright.config.ts b/apps/web/playwright.config.ts index d9e98a7..c747b8a 100644 --- a/apps/web/playwright.config.ts +++ b/apps/web/playwright.config.ts @@ -1,5 +1,8 @@ +/* eslint-disable */ import { defineConfig, devices } from '@playwright/test'; +const baseURL = 'http://localhost:3000'; + export default defineConfig({ testDir: './e2e', fullyParallel: true, @@ -8,7 +11,7 @@ export default defineConfig({ workers: process.env.CI ? 1 : undefined, reporter: 'html', use: { - baseURL: 'http://localhost:3000', + baseURL: baseURL, trace: 'on-first-retry', headless: !!process.env.CI, }, @@ -28,7 +31,7 @@ export default defineConfig({ ], webServer: { command: 'pnpm dev', - url: 'http://localhost:3000', + url: baseURL, reuseExistingServer: !process.env.CI, }, }); diff --git a/apps/web/plugins/sse-plugin.ts b/apps/web/plugins/sse-plugin.ts new file mode 100644 index 0000000..777d247 --- /dev/null +++ b/apps/web/plugins/sse-plugin.ts @@ -0,0 +1,38 @@ +import type { IncomingMessage, ServerResponse } from 'node:http'; +import { type Plugin, type ViteDevServer } from 'vite'; + +/** + * Custom Vite plugin that provides a mock Server-Sent Events (SSE) stream at `/api/sse` for live testing. + */ +export function ssePlugin(): Plugin { + return { + name: 'sse-plugin', + configureServer(server: ViteDevServer) { + server.middlewares.use('/api/sse', (req: IncomingMessage, res: ServerResponse) => { + res.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + Connection: 'keep-alive', + 'Access-Control-Allow-Origin': '*', + }); + + res.write( + `data: ${JSON.stringify({ event: 'connected', message: 'SSE Stream Active', timestamp: new Date().toISOString() })}\n\n` + ); + + const timer = setInterval(() => { + const payload = JSON.stringify({ + event: 'ticker', + timestamp: new Date().toISOString(), + status: 'online', + }); + res.write(`data: ${payload}\n\n`); + }, 3000); + + req.on('close', () => { + clearInterval(timer); + }); + }); + }, + }; +} diff --git a/apps/web/src/features/privacy-modal/ui/privacy-modal.tsx b/apps/web/src/features/privacy-modal/ui/privacy-modal.tsx deleted file mode 100644 index d9ea491..0000000 --- a/apps/web/src/features/privacy-modal/ui/privacy-modal.tsx +++ /dev/null @@ -1,167 +0,0 @@ -'use client'; - -import React, { useEffect, useState } from 'react'; -import { Link } from 'react-router-dom'; -import { ShieldCheck, X, Lock, ArrowRight, CheckCircle2 } from 'lucide-react'; -import { Button } from '@/shared/ui/button'; - -interface PrivacyModalProps { - isOpen: boolean; - onClose: () => void; -} - -export const PrivacyModal: React.FC = ({ isOpen, onClose }) => { - const [rendered, setRendered] = useState(isOpen); - const [visible, setVisible] = useState(false); - - useEffect(() => { - if (isOpen) { - setRendered(true); - document.body.style.overflow = 'hidden'; - // Trigger smooth CSS fade in on next tick - const timer = setTimeout(() => setVisible(true), 10); - return () => clearTimeout(timer); - } else { - setVisible(false); - document.body.style.overflow = ''; - // Wait for exit transition to complete before unmounting - const timer = setTimeout(() => setRendered(false), 200); - return () => clearTimeout(timer); - } - }, [isOpen]); - - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Escape') onClose(); - }; - if (isOpen) { - window.addEventListener('keydown', handleKeyDown); - } - return () => { - window.removeEventListener('keydown', handleKeyDown); - }; - }, [isOpen, onClose]); - - if (!rendered) return null; - - return ( -
-
e.stopPropagation()} - > - {/* Header */} -
-
-
- -
-
-

- Privacy Policy -

-

- mobile-devtools Documentation & Live Demo -

-
-
- -
- - {/* Content */} -
-
- -
- Zero Telemetry Promise: This website - is purely interactive documentation & live demo for{' '} - mobile-devtools. We collect zero personal data, - store zero tracking cookies, and run 100% locally in your browser. -
-
- -
-

- 1. What is this Website? -

-

- This website serves as the official documentation hub and live interactive demo - environment for{' '} - mobile-devtools, an - open-source React component for in-app mobile browser debugging. -

-
- -
-

- 2. Zero Data Collection Guarantee -

-

- We prioritize your privacy above all else. We do{' '} - NOT: -

-
    -
  • - - Collect, store, or transmit personal data or IP addresses. -
  • -
  • - - Use analytics scripts, trackers, telemetry, or fingerprinting. -
  • -
  • - - Set non-essential cookies or third-party tracking identifiers. -
  • -
  • - - Send your console logs or network requests to any remote server. -
  • -
-
- -
-

- 3. In-Browser Local Isolation -

-

- All devtools interactions, network request interception demos, console log capturing, - and storage inspections occur exclusively inside your browser memory within Shadow - DOM. -

-
-
- - {/* Footer actions */} -
- - Full Privacy Document - - - -
-
-
- ); -}; diff --git a/apps/web/src/shared/ui/button.tsx b/apps/web/src/shared/ui/button.tsx index a30c663..446013f 100644 --- a/apps/web/src/shared/ui/button.tsx +++ b/apps/web/src/shared/ui/button.tsx @@ -12,6 +12,7 @@ export const buttonVariants = cva( outline: 'bg-transparent text-dev-text-bright border border-dev-border hover:bg-dev-bg-300', ghost: 'bg-transparent text-dev-text-muted hover:text-dev-text-bright hover:bg-dev-bg-300', contrast: 'bg-neutral-900 text-white dark:bg-neutral-950 dark:text-white hover:bg-neutral-800 dark:hover:bg-neutral-900 border-2 border-neutral-700 dark:border-neutral-600', + danger: 'bg-rose-500/10 text-rose-600 dark:text-rose-400 hover:bg-rose-500/20 border border-rose-500/30 font-semibold', }, size: { sm: 'px-3 py-1.5 text-xs', diff --git a/apps/web/src/widgets/devtools-wrapper/ui/devtools-wrapper.tsx b/apps/web/src/widgets/devtools-wrapper/ui/devtools-wrapper.tsx deleted file mode 100644 index bb24494..0000000 --- a/apps/web/src/widgets/devtools-wrapper/ui/devtools-wrapper.tsx +++ /dev/null @@ -1,17 +0,0 @@ -'use client'; - -import { MobileDevTools } from 'mobile-devtools/react'; - -export function DevToolsWrapper() { - return ( - - ); -} diff --git a/apps/web/src/widgets/test-harness/ui/test-harness.tsx b/apps/web/src/widgets/test-harness/ui/test-harness.tsx index a399985..7b7a80d 100644 --- a/apps/web/src/widgets/test-harness/ui/test-harness.tsx +++ b/apps/web/src/widgets/test-harness/ui/test-harness.tsx @@ -1,10 +1,29 @@ 'use client'; import React from 'react'; -import { Terminal, Globe, Database, Play, AlertTriangle, AlertCircle } from 'lucide-react'; +import { + Terminal, + Globe, + Database, + Play, + AlertTriangle, + AlertCircle, + Zap, +} from 'lucide-react'; import { Button } from '@/shared/ui/button'; export const TestHarness: React.FC = () => { + const sseRef = React.useRef(null); + const [isSSESubscribed, setIsSSESubscribed] = React.useState(false); + + React.useEffect(() => { + return () => { + if (sseRef.current) { + sseRef.current.close(); + sseRef.current = null; + } + }; + }, []); const triggerConsoleLog = () => { console.log('๐Ÿš€ User triggered console log', { timestamp: new Date().toISOString(), @@ -42,7 +61,11 @@ export const TestHarness: React.FC = () => { const res = await fetch('https://jsonplaceholder.typicode.com/posts/error-500', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ title: 'Simulated Danger Error', body: 'Testing 500 Internal Server Error', userId: 99 }), + body: JSON.stringify({ + title: 'Simulated Danger Error', + body: 'Testing 500 Internal Server Error', + userId: 99, + }), }); if (!res.ok) { console.error('๐Ÿ”ฅ POST /posts (500) Simulated Danger Error:', { @@ -82,12 +105,84 @@ export const TestHarness: React.FC = () => { console.log('Set demo cookie entry'); }; + const triggerWebSocketDemo = () => { + try { + const ws = new WebSocket('wss://echo.websocket.org'); + ws.onopen = () => { + console.log('โšก WebSocket Connected to wss://echo.websocket.org'); + ws.send( + JSON.stringify({ + event: 'ping', + payload: { client: 'mobile-devtools-demo', timestamp: Date.now() }, + }) + ); + }; + ws.onmessage = (evt) => { + console.log('โšก WebSocket Message Received:', evt.data); + }; + ws.onerror = (err) => { + console.error('โšก WebSocket Error:', err); + }; + } catch (e) { + console.error('WebSocket simulation error:', e); + } + }; + + const toggleSSEDemo = () => { + if (isSSESubscribed && sseRef.current) { + sseRef.current.close(); + sseRef.current = null; + setIsSSESubscribed(false); + console.log('๐Ÿ“ก SSE Connection Closed by User'); + return; + } + + try { + const sseUrl = '/api/sse'; + const es = new EventSource(sseUrl); + sseRef.current = es; + setIsSSESubscribed(true); + + es.onopen = () => { + console.log(`๐Ÿ“ก SSE Connection Established to ${sseUrl}`); + }; + es.onmessage = (evt) => { + console.log('๐Ÿ“ก SSE Stream Event Received:', evt.data); + }; + es.onerror = (err) => { + console.warn('๐Ÿ“ก SSE Connection Error / Closed:', err); + }; + } catch (e) { + console.error('SSE simulation error:', e); + } + }; + + const triggerShakeDemo = () => { + // 1. Establish baseline rest acceleration + const baseEvt = Object.assign(new Event('devicemotion'), { + accelerationIncludingGravity: { x: 0, y: 0, z: 9.8 }, + }); + window.dispatchEvent(baseEvt); + + // 2. Dispatch high acceleration motion burst + const shakeEvt = Object.assign(new Event('devicemotion'), { + accelerationIncludingGravity: { x: 25, y: 25, z: 25 }, + }); + window.dispatchEvent(shakeEvt); + + console.log('๐Ÿ“ฑ Simulated Device Shake Event Dispatched!'); + }; + return (
-

Interactive Test Controls

+

+ Interactive Test Controls +

- Click any action button below and tap the floating DevTools badge in the corner to inspect captured data! + Click any action button below and tap the floating{' '} + DevTools badge in the corner to inspect + captured data!

@@ -98,17 +193,29 @@ export const TestHarness: React.FC = () => {

1. Console Interceptor Test

-

Trigger console entries to test badge counter & stack trace formatting.

+

+ Trigger console entries to test badge counter & stack trace formatting. +

- - @@ -119,19 +226,33 @@ export const TestHarness: React.FC = () => {
-

2. Network (Fetch & XHR) Test

+

+ 2. Network (Fetch & XHR) Test +

-

Trigger real HTTP requests to test timing, JSON body, & Copy cURL.

+

+ Trigger real HTTP requests to test timing, JSON body, & Copy cURL. +

- - @@ -144,7 +265,10 @@ export const TestHarness: React.FC = () => {

3. Storage Inspector Test

-

Mutate localStorage, sessionStorage, and document.cookie to test the Storage Inspector tab.

+

+ Mutate localStorage, sessionStorage, and document.cookie to test the Storage Inspector + tab. +

+ + {/* WebSocket, SSE & Motion Test Card */} +
+
+ +

+ 4. WebSocket, SSE & Shake Motion Test +

+
+

+ Simulate real-time WebSocket frames, EventSource streams, and device shake gesture. +

+
+ + + +
+
); diff --git a/apps/web/src/widgets/theme-playground/ui/theme-playground.tsx b/apps/web/src/widgets/theme-playground/ui/theme-playground.tsx index ead9a50..48f6f98 100644 --- a/apps/web/src/widgets/theme-playground/ui/theme-playground.tsx +++ b/apps/web/src/widgets/theme-playground/ui/theme-playground.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react/prop-types */ 'use client'; import React, { useState, useRef } from 'react'; @@ -17,6 +18,8 @@ export const ThemePlayground: React.FC = () => { const [cardBg, setCardBg] = useState(''); const [defaultOpenConfig, setDefaultOpenConfig] = useState(false); const [autoSnapBadge, setAutoSnapBadge] = useState(false); + const [showBadge, setShowBadge] = useState(true); + const [shakeToToggle, setShakeToToggle] = useState(true); const [maxLogLimit, setMaxLogLimit] = useState(200); const [enabledTabs, setEnabledTabs] = useState([ 'console', @@ -115,6 +118,8 @@ export const ThemePlayground: React.FC = () => { ` initialTab="${initialTab}"`, ` defaultOpen={${defaultOpenConfig}}`, ` autoSnapBadge={${autoSnapBadge}}`, + showBadge === false ? ` showBadge={false}` : null, + shakeToToggle ? ` shakeToToggle={true}` : null, ` enabledTabs={[${enabledTabs.map((t) => `'${t}'`).join(', ')}]}`, ` theme={{\n mode: '${mode}',\n accentColor: '${accentColor || '#38bdf8'}',\n backgroundColor: '${bgColor || '#0f172a'}',\n cardBackgroundColor: '${cardBg || '#1e293b'}'\n }}`, ` interceptors={{\n maxLogLimit: ${maxLogLimit},\n maxNetworkLimit: 100,\n enableConsoleInterceptor: true,\n enableFetchInterceptor: true,\n enableXhrInterceptor: true\n }}`, @@ -168,6 +173,20 @@ export const ThemePlayground: React.FC = () => { {autoSnapBadge ? 'true' : 'false'} {'}'} + {showBadge === false && ( +
+ showBadge={'{'} + false + {'}'} +
+ )} + {shakeToToggle && ( +
+ shakeToToggle={'{'} + true + {'}'} +
+ )}
enabledTabs={'{'}[ {enabledTabs.map((t, idx) => ( @@ -260,6 +279,8 @@ export const ThemePlayground: React.FC = () => { position={position} initialTab={initialTab} autoSnapBadge={autoSnapBadge} + showBadge={showBadge} + shakeToToggle={shakeToToggle} enabledTabs={enabledTabs} theme={{ mode, @@ -571,7 +592,7 @@ export const ThemePlayground: React.FC = () => {
{/* Custom Styled Checkboxes */} -
+
+ + + +
@@ -628,7 +683,7 @@ export const ThemePlayground: React.FC = () => { ) : ( <> - Copy Configuration + Copy )} diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 4c5f756..e4aace5 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -3,9 +3,10 @@ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import { cloudflare } from '@cloudflare/vite-plugin'; import { getDevToolsAliases } from '@mobile-devtools/vite-config'; +import { ssePlugin } from './plugins/sse-plugin'; export default defineConfig({ - plugins: [react(), cloudflare()], + plugins: [react(), cloudflare(), ssePlugin()], resolve: { alias: { '@': resolve(__dirname, './src'), @@ -14,6 +15,5 @@ export default defineConfig({ }, server: { port: 3000, - allowedHosts: ['campaign-beauty-canvas-its.trycloudflare.com'], }, }); diff --git a/apps/web/worker-configuration.d.ts b/apps/web/worker-configuration.d.ts index 9595534..245e757 100644 --- a/apps/web/worker-configuration.d.ts +++ b/apps/web/worker-configuration.d.ts @@ -10726,7 +10726,7 @@ type AIGatewayHeaders = { [key: string]: string | number | boolean | object; }; type AIGatewayUniversalRequest = { - provider: AIGatewayProviders | string; // eslint-disable-line + provider: AIGatewayProviders | string; endpoint: string; headers: Partial; query: unknown; @@ -10743,7 +10743,7 @@ declare abstract class AiGateway { extraHeaders?: object; signal?: AbortSignal; }): Promise; - getUrl(provider?: AIGatewayProviders | string): Promise; // eslint-disable-line + getUrl(provider?: AIGatewayProviders | string): Promise; } // Copyright (c) 2022-2025 Cloudflare, Inc. // Licensed under the Apache 2.0 license found in the LICENSE file or at: diff --git a/examples/svelte/index.html b/examples/svelte/index.html new file mode 100644 index 0000000..b25f510 --- /dev/null +++ b/examples/svelte/index.html @@ -0,0 +1,43 @@ + + + + + + Mobile DevTools - Svelte Example + + + +
+ + + diff --git a/examples/svelte/package.json b/examples/svelte/package.json new file mode 100644 index 0000000..3442d36 --- /dev/null +++ b/examples/svelte/package.json @@ -0,0 +1,21 @@ +{ + "name": "example-svelte", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "mobile-devtools": "workspace:*", + "svelte": "catalog:" + }, + "devDependencies": { + "@mobile-devtools/tsconfig": "workspace:*", + "@mobile-devtools/vite-config": "workspace:*", + "@sveltejs/vite-plugin-svelte": "catalog:", + "typescript": "catalog:", + "vite": "catalog:" + } +} diff --git a/examples/svelte/src/app.svelte b/examples/svelte/src/app.svelte new file mode 100644 index 0000000..eb43348 --- /dev/null +++ b/examples/svelte/src/app.svelte @@ -0,0 +1,42 @@ + + +
+

๐Ÿ”ฅ Mobile DevTools - Svelte Example

+

+ Count: {count} +

+
+ + + +
+
diff --git a/examples/svelte/src/main.ts b/examples/svelte/src/main.ts new file mode 100644 index 0000000..0958f2f --- /dev/null +++ b/examples/svelte/src/main.ts @@ -0,0 +1,8 @@ +import { mount } from 'svelte'; +import App from './app.svelte'; + +const app = mount(App, { + target: document.getElementById('app')!, +}); + +export default app; diff --git a/examples/svelte/tsconfig.json b/examples/svelte/tsconfig.json new file mode 100644 index 0000000..a0cbef1 --- /dev/null +++ b/examples/svelte/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@mobile-devtools/tsconfig/base.json", + "compilerOptions": { + "noEmit": true, + "allowImportingTsExtensions": true + }, + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.svelte"] +} diff --git a/examples/svelte/vite.config.ts b/examples/svelte/vite.config.ts new file mode 100644 index 0000000..f47266d --- /dev/null +++ b/examples/svelte/vite.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'vite'; +import { svelte } from '@sveltejs/vite-plugin-svelte'; +import { getDevToolsAliases } from '@mobile-devtools/vite-config'; + +export default defineConfig({ + plugins: [svelte()], + resolve: { + alias: getDevToolsAliases(__dirname, '../..'), + conditions: ['browser'], + }, + server: { + port: 3003, + }, +}); diff --git a/packages/config/vite/index.js b/packages/config/vite/index.js index e21760c..0b24a5c 100644 --- a/packages/config/vite/index.js +++ b/packages/config/vite/index.js @@ -10,6 +10,7 @@ export function getDevToolsAliases(rootDir, relativeMonorepoRoot = '../..') { return { 'mobile-devtools/react': resolve(monorepoRoot, 'packages/mobile-devtools/src/react.ts'), 'mobile-devtools/vue': resolve(monorepoRoot, 'packages/mobile-devtools/src/vue.ts'), + 'mobile-devtools/svelte': resolve(monorepoRoot, 'packages/mobile-devtools/src/svelte.ts'), 'mobile-devtools/package.json': resolve(monorepoRoot, 'packages/mobile-devtools/package.json'), 'mobile-devtools$': resolve(monorepoRoot, 'packages/mobile-devtools/src/index.ts'), }; diff --git a/packages/mobile-devtools/CHANGELOG.md b/packages/mobile-devtools/CHANGELOG.md index 7e57744..489c6d8 100644 --- a/packages/mobile-devtools/CHANGELOG.md +++ b/packages/mobile-devtools/CHANGELOG.md @@ -4,6 +4,27 @@ All notable changes to the `mobile-devtools` package will be documented in this The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.3.0] - 2026-08-17 + +### โšก Real-time SSE/WebSocket Interception & Shake-to-Toggle Motion Detection + +#### Added + +- **Svelte 4/5 Framework Adapter**: Added first-class Svelte adapter (`mobile-devtools/svelte`) with reactive store support (`mobileDevTools`), `useMobileDevTools()` action directive, and `peerDependencies` declarations. +- **Real-time EventSource (SSE) Stream Interception**: Intercepts native SSE streams (`text/event-stream`), custom event listeners (`es.addEventListener(...)`), and streams live payload updates directly to the Network tab. +- **WebSocket Real-time Frame Interception**: Captures incoming and outgoing WebSocket frames with live payload inspection and real-time tab frame counter updates. +- **`shakeToToggle` Physical Motion Gesture**: Toggle the DevTools overlay by shaking physical mobile devices. Built with a low-pass gravity filter sensor fusion and seamless iOS Safari permission request handling. +- **Smart Scroll Preserving & Auto-scroll**: Preserves exact list scroll position when inspecting past logs/frames during fast live streaming, and auto-scrolls to bottom when monitoring live activity. +- **Full TSDoc/JSDoc Annotations**: Comprehensive IDE hover tooltips, `@default` values, and `@example` code blocks across all types, `DevToolsConfig`, and utility functions. + +#### Fixed & Improved + +- **Badge Count Cap**: Standardized all unread count badges and counter labels to cap at `99+`. +- **Crypto-based `generateId(prefix)`**: Created collision-free ID generator using `crypto.randomUUID()` with fallback. +- **Style Source of Truth**: Consolidated badge custom styling to `config.styles?.badge` and removed redundant top-level `badgeStyle` prop. +- **Dead Code Cleanup**: Audited `apps/web/src` and removed unused `DevToolsWrapper` component. + +--- ## [1.2.0] - 2026-08-17 diff --git a/packages/mobile-devtools/README.md b/packages/mobile-devtools/README.md index 04eba3a..3288bb4 100644 --- a/packages/mobile-devtools/README.md +++ b/packages/mobile-devtools/README.md @@ -1,31 +1,52 @@ -# mobile-devtools +# Mobile DevTools > **Next-Gen Framework-Agnostic In-App Mobile Debugger & Inspector Overlay for Web Applications** [![Bundle Size](https://img.shields.io/badge/Bundle_Size-~2.0_kB_gzipped-10b981.svg)](https://bundlephobia.com/) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-3178c6.svg)](https://www.typescriptlang.org/) +[![Turborepo](https://img.shields.io/badge/Monorepo-Turborepo-ef4444.svg)](https://turbo.build/) [![React](https://img.shields.io/badge/Adapter-React_18%2F19-61dafb.svg)](https://react.dev/) [![Vue](https://img.shields.io/badge/Adapter-Vue_3-42b883.svg)](https://vuejs.org/) +[![Svelte](https://img.shields.io/badge/Adapter-Svelte_4%2F5-ff3e00.svg)](https://svelte.dev/) [![Vanilla JS](https://img.shields.io/badge/Adapter-Vanilla_JS-f7df1e.svg)](https://developer.mozilla.org/en-US/docs/Web/JavaScript) --- ## ๐Ÿ“Œ Table of Contents +- [๐Ÿ“š Comprehensive Documentation Suite](./docs) +- [๐Ÿ“ธ Showcase](#-showcase) - [๐Ÿ’ก Motivation & Why Use It?](#-motivation--why-use-it) - [โœจ Core Capabilities](#-core-capabilities) - [๐Ÿ—๏ธ Technical Architecture](#%EF%B8%8F-technical-architecture) - [๐Ÿš€ Framework Quickstart](#-framework-quickstart) - [โš›๏ธ React Integration](#%EF%B8%8F-react-integration) - [๐Ÿ’š Vue 3 Integration](#-vue-3-integration) + - [๐Ÿงก Svelte 4/5 Integration](#-svelte-45-integration) - [๐Ÿฆ Vanilla JS / Legacy Apps](#-vanilla-js--legacy-apps) - [โš™๏ธ Full Configuration & Props Reference](#%EF%B8%8F-full-configuration--props-reference) - [๐ŸŽจ Theme Engine & Customization](#-theme-engine--customization) +- [๐Ÿ“‚ Monorepo Structure](#-monorepo-structure) +- [๐Ÿ› ๏ธ Development Setup](#%EF%B8%8F-development-setup) - [๐Ÿ“„ License](#-license) --- +## ๐Ÿ“ธ Showcase + +

+ Console Tab + Elements Tab + Network Tab +

+

+ Storage Tab + System Tab +

+ +--- + ## ๐Ÿ’ก Motivation & Why Use It? Debugging mobile web applications or QA staging builds on physical smartphones, tablets, or embedded webviews is historically painful: @@ -55,28 +76,28 @@ Debugging mobile web applications or QA staging builds on physical smartphones, - ๐ŸŽจ **Granular UI Style Overrides (`styles`)**: Fine-grained inline CSS style overrides for badge, drawer, overlay, and handle (`styles={{ badge: {}, drawer: {}, overlay: {} }}`). - ๐ŸŽจ **Dynamic Theme Engine**: Built-in Light Mode and Dark Mode with auto-contrast luminance detection, accent color swatches, and custom background palettes. - ๐Ÿงช **Comprehensive Test Suite**: Tested with **65 Unit Tests (100% Passed)** + **21 Playwright E2E Tests (100% Passed)** across Desktop Chrome, Mobile Chrome, and Mobile Safari. -- ๐Ÿงฉ **Framework Agnostic**: Native support for **React 18/19**, **Vue 3**, and **Vanilla JS**. +- ๐Ÿงฉ **Framework Agnostic**: Native support for **React 18/19**, **Vue 3**, **Svelte 4/5**, and **Vanilla JS**. --- ## ๐Ÿ—๏ธ Technical Architecture ```text -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ mobile-devtools โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ”‚ - โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” - โ–ผ โ–ผ โ–ผ - Vanilla JS React Vue 3 - (mobile-devtools) (mobile-devtools/react) (mobile-devtools/vue) - โ”‚ โ”‚ โ”‚ - โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ”‚ - โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” - โ”‚ Native Shadow DOM Host โ”‚ - โ”‚ โ”‚ - โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ mobile-devtools โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ–ผ โ–ผ โ–ผ โ–ผ + Vanilla JS React Vue 3 Svelte +(mobile-devtools) (mobile-devtools/react) (mobile-devtools/vue) (mobile-devtools/svelte) + โ”‚ โ”‚ โ”‚ โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ Native Shadow DOM Host โ”‚ + โ”‚ โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ @@ -178,6 +199,45 @@ Import from `mobile-devtools/vue`: --- +### ๐Ÿ”ฅ Svelte Integration + +Import from `mobile-devtools/svelte`: + +```svelte + + +
+ +
+``` + +### ๐Ÿงก Svelte 4/5 Integration + +Import from `mobile-devtools/svelte`: + +```svelte + + +
+ +
+``` + +--- + ### ๐Ÿฆ Vanilla JS / Legacy Apps Import directly from `mobile-devtools`: @@ -216,25 +276,25 @@ const devtools = createMobileDevTools({ Below is the complete reference table for all configuration options supported by `` / `createMobileDevTools()`: -| Option / Prop | Type | Default | Description | -| :-------------------------- | :---------------------- | :----------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------ | -| `enabled` | `boolean` | `true` (in dev) | Enable or disable the DevTools overlay. Automatically set to `false` in production builds. | -| `forceEnable` | `boolean` | `false` | Force enable DevTools overlay in production builds for QA testing & staging previews. | -| `title` | `string` | `'DevTools'` | Label shown on floating badge and drawer header | -| `icon` | `string` | `undefined` | Custom icon (Emoji string like `'โšก'`, Image URL, or Base64 data URI) | -| `position` | `BadgePositionPreset` | `'bottom-right'` | Initial corner/edge preset (`'bottom-right'`, `'bottom-left'`, `'top-right'`, `'top-left'`, `'bottom'`, `'top'`, `'left'`, `'right'`) | -| `initialTab` | `DevToolsTabId` | `'console'` | Default tab opened when drawer is triggered (`'console'`, `'elements'`, `'network'`, `'storage'`, `'system'`) | -| `enabledTabs` | `DevToolsTabId[]` | `['console', 'elements', 'network', 'storage', 'system']` | Filter which tabs are enabled in drawer | -| `customTabs` | `CustomTabDefinition[]` | `[]` | Pluggable consumer tabs with custom DOM rendering callback (`render(container)`) | -| `styles` | `DevToolsStyles` | `undefined` | Fine-grained custom style overrides object (`{ badge?: {}, drawer?: {}, overlay?: {}, handle?: {} }`) | -| `defaultOpen` | `boolean` | `false` | Set to `true` to open drawer automatically on mount | -| `autoSnapBadge` | `boolean` | `false` | Enable magnetic snapping of badge to nearest screen edge on drag release | -| `theme.mode` | `'dark' \| 'light'` | `'dark'` | Theme mode | -| `theme.accentColor` | `string` | `undefined` | Custom primary accent color (Hex / RGB / HSL) | -| `theme.backgroundColor` | `string` | `undefined` | Custom background color for drawer and badge | -| `theme.cardBackgroundColor` | `string` | `undefined` | Custom background color for inner card elements | -| `privacy.mask` | `string[]` | `undefined` | Sensitive header & body keys to mask in network inspector (e.g. `['token', 'password']`) | -| `interceptors.maxLogLimit` | `number` | `200` | Maximum number of console logs stored in buffer | +| Option / Prop | Type | Default | Description | +| :-------------------------- | :---------------------- | :-------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------ | +| `enabled` | `boolean` | `true` (in dev) | Enable or disable the DevTools overlay. Automatically set to `false` in production builds. | +| `forceEnable` | `boolean` | `false` | Force enable DevTools overlay in production builds for QA testing & staging previews. | +| `title` | `string` | `'DevTools'` | Label shown on floating badge and drawer header | +| `icon` | `string` | `undefined` | Custom icon (Emoji string like `'โšก'`, Image URL, or Base64 data URI) | +| `position` | `BadgePositionPreset` | `'bottom-right'` | Initial corner/edge preset (`'bottom-right'`, `'bottom-left'`, `'top-right'`, `'top-left'`, `'bottom'`, `'top'`, `'left'`, `'right'`) | +| `initialTab` | `DevToolsTabId` | `'console'` | Default tab opened when drawer is triggered (`'console'`, `'elements'`, `'network'`, `'storage'`, `'system'`) | +| `enabledTabs` | `DevToolsTabId[]` | `['console', 'elements', 'network', 'storage', 'system']` | Filter which tabs are enabled in drawer | +| `customTabs` | `CustomTabDefinition[]` | `[]` | Pluggable consumer tabs with custom DOM rendering callback (`render(container)`) | +| `styles` | `DevToolsStyles` | `undefined` | Fine-grained custom style overrides object (`{ badge?: {}, drawer?: {}, overlay?: {}, handle?: {} }`) | +| `defaultOpen` | `boolean` | `false` | Set to `true` to open drawer automatically on mount | +| `autoSnapBadge` | `boolean` | `false` | Enable magnetic snapping of badge to nearest screen edge on drag release | +| `theme.mode` | `'dark' \| 'light'` | `'dark'` | Theme mode | +| `theme.accentColor` | `string` | `undefined` | Custom primary accent color (Hex / RGB / HSL) | +| `theme.backgroundColor` | `string` | `undefined` | Custom background color for drawer and badge | +| `theme.cardBackgroundColor` | `string` | `undefined` | Custom background color for inner card elements | +| `privacy.mask` | `string[]` | `undefined` | Sensitive header & body keys to mask in network inspector (e.g. `['token', 'password']`) | +| `interceptors.maxLogLimit` | `number` | `200` | Maximum number of console logs stored in buffer | --- @@ -261,6 +321,55 @@ Below is the complete reference table for all configuration options supported by --- +## ๐Ÿ“‚ Monorepo Structure + +``` +mobile-devtools/ +โ”œโ”€โ”€ apps/ +โ”‚ โ””โ”€โ”€ web/ # React documentation & live playground app (Port 3000) +โ”œโ”€โ”€ examples/ +โ”‚ โ”œโ”€โ”€ react/ # React 19 test harness app (Port 3001) +โ”‚ โ”œโ”€โ”€ vue/ # Vue 3 test harness app (Port 3002) +โ”‚ โ””โ”€โ”€ vanilla/ # Vanilla JS test harness app (Port 3003) +โ””โ”€โ”€ packages/ + โ”œโ”€โ”€ mobile-devtools/ # Main unified published npm package (Core + UI + React/Vue/Vanilla Adapters) + โ””โ”€โ”€ config/ + โ”œโ”€โ”€ eslint/ # Shared ESLint configuration (@mobile-devtools/eslint-config) + โ””โ”€โ”€ typescript/ # Shared TypeScript configuration (@mobile-devtools/tsconfig) +``` + +--- + +## ๐Ÿ› ๏ธ Development Setup + +To build and run the project locally: + +```bash +# Clone repository +git clone https://github.com/dewasemadi/mobile-devtools.git +cd mobile-devtools + +# Install dependencies using pnpm +pnpm install + +# Launch all apps & package watchers in dev mode +pnpm dev + +# Run unit test suite (65 tests) +pnpm test + +# Run unit tests with V8 coverage report +pnpm test:coverage + +# Run Playwright E2E tests (21 tests across Chromium & Mobile Webkit) +pnpm test:e2e + +# Build production bundles +pnpm build +``` + +--- + ## ๐Ÿ“„ License -Distributed under the **MIT License**. +Distributed under the **MIT License**. See [LICENSE](./LICENSE) for details. diff --git a/packages/mobile-devtools/package.json b/packages/mobile-devtools/package.json index 0a26d7e..8232e84 100644 --- a/packages/mobile-devtools/package.json +++ b/packages/mobile-devtools/package.json @@ -1,6 +1,6 @@ { "name": "mobile-devtools", - "version": "1.2.0", + "version": "1.3.0", "description": "Next-Gen Framework-Agnostic In-App Mobile Debugger & Inspector Overlay for Web Applications", "license": "MIT", "type": "module", @@ -34,6 +34,11 @@ "import": "./dist/vue.js", "default": "./dist/vue.js" }, + "./svelte": { + "types": "./dist/svelte.d.ts", + "import": "./dist/svelte.js", + "default": "./dist/svelte.js" + }, "./package.json": "./package.json" }, "files": [ @@ -54,6 +59,7 @@ "network", "react", "vue", + "svelte", "vanilla", "javascript", "typescript", @@ -62,6 +68,7 @@ "vconsole" ], "scripts": { + "prebuild": "cp ../../README.md ./README.md", "check-types": "tsc --noEmit", "build": "vite build", "clean": "rm -rf dist tsconfig.tsbuildinfo", @@ -71,6 +78,7 @@ "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", + "svelte": "^4.0.0 || ^5.0.0", "vue": "^3.0.0" }, "peerDependenciesMeta": { @@ -80,6 +88,9 @@ "react-dom": { "optional": true }, + "svelte": { + "optional": true + }, "vue": { "optional": true } @@ -94,6 +105,7 @@ "jsdom": "catalog:", "react": "catalog:", "react-dom": "catalog:", + "svelte": "catalog:", "typescript": "catalog:", "vite": "catalog:", "vite-plugin-dts": "catalog:", diff --git a/packages/mobile-devtools/src/adapters/react/components/index.tsx b/packages/mobile-devtools/src/adapters/react/components/index.tsx index 2fc82d2..c155efa 100644 --- a/packages/mobile-devtools/src/adapters/react/components/index.tsx +++ b/packages/mobile-devtools/src/adapters/react/components/index.tsx @@ -2,10 +2,34 @@ import React, { useEffect, useRef } from 'react'; import { DevToolsConfig } from '../../../core'; import { MobileDevToolsEngine } from '../../../ui'; +/** + * Props for the React `` overlay component. + */ export interface MobileDevToolsProps extends DevToolsConfig { + /** Optional nested configuration object */ config?: DevToolsConfig; } +/** + * Mobile DevTools React Component. + * Mounts an isolated Shadow DOM in-app debugger overlay for React applications. + * + * @example + * ```tsx + * import { MobileDevTools } from 'mobile-devtools/react'; + * + * export function App() { + * return ( + * + * ); + * } + * ``` + */ export const MobileDevTools: React.FC = (props) => { const { config, ...restProps } = props; const mergedConfig = config ? { ...config, ...restProps } : restProps; diff --git a/packages/mobile-devtools/src/adapters/svelte/__tests__/svelte.test.ts b/packages/mobile-devtools/src/adapters/svelte/__tests__/svelte.test.ts new file mode 100644 index 0000000..fec1e51 --- /dev/null +++ b/packages/mobile-devtools/src/adapters/svelte/__tests__/svelte.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vitest'; +import { mobileDevTools, useMobileDevTools } from '../index'; + +describe('Svelte Adapter', () => { + it('should initialize via useMobileDevTools composable hook', () => { + const instance = useMobileDevTools({ title: 'Svelte Hook Test' }); + expect(instance.store).not.toBeNull(); + + const shadowHost = document.querySelector('mobile-devtools-root'); + expect(shadowHost).not.toBeNull(); + + instance.destroy(); + + const shadowHostAfterDestroy = document.querySelector('mobile-devtools-root'); + expect(shadowHostAfterDestroy).toBeNull(); + }); + + it('should mount via mobileDevTools action directive', () => { + const container = document.createElement('div'); + document.body.appendChild(container); + + const action = mobileDevTools(container, { title: 'Svelte Action Test' }); + const shadowHost = document.querySelector('mobile-devtools-root'); + expect(shadowHost).not.toBeNull(); + + action.destroy(); + document.body.removeChild(container); + + const shadowHostAfterDestroy = document.querySelector('mobile-devtools-root'); + expect(shadowHostAfterDestroy).toBeNull(); + }); +}); diff --git a/packages/mobile-devtools/src/adapters/svelte/index.ts b/packages/mobile-devtools/src/adapters/svelte/index.ts new file mode 100644 index 0000000..85fdcec --- /dev/null +++ b/packages/mobile-devtools/src/adapters/svelte/index.ts @@ -0,0 +1,44 @@ +import { DevToolsConfig, DevToolsStore } from '../../core'; +import { MobileDevToolsEngine } from '../../ui'; + +export interface SvelteDevToolsInstance { + engine: MobileDevToolsEngine; + store: DevToolsStore; + destroy: () => void; +} + +/** + * Svelte composable hook to mount MobileDevTools lifecycle. + */ +export function useMobileDevTools(config?: DevToolsConfig): SvelteDevToolsInstance { + const engine = new MobileDevToolsEngine(config); + const { store } = engine.mount(); + + return { + engine, + store, + destroy: () => engine.destroy(), + }; +} + +/** + * Svelte Action directive: `
` + */ +export function mobileDevTools(node: HTMLElement, config?: DevToolsConfig) { + const engine = new MobileDevToolsEngine({ + ...config, + container: node, + }); + engine.mount(); + + return { + update(newConfig: DevToolsConfig) { + engine.updateConfig(newConfig); + }, + destroy() { + engine.destroy(); + }, + }; +} + +export type { DevToolsConfig, DevToolsStore }; diff --git a/packages/mobile-devtools/src/adapters/vue/index.ts b/packages/mobile-devtools/src/adapters/vue/index.ts index c9cca96..e392e7b 100644 --- a/packages/mobile-devtools/src/adapters/vue/index.ts +++ b/packages/mobile-devtools/src/adapters/vue/index.ts @@ -39,7 +39,6 @@ export const MobileDevTools = defineComponent({ enabledTabs: { type: Array as PropType, default: undefined }, autoSnapBadge: { type: Boolean as PropType, default: undefined }, styles: { type: Object as PropType, default: undefined }, - badgeStyle: { type: Object as PropType, default: undefined }, interceptors: { type: Object as PropType, default: undefined }, }, setup(props) { diff --git a/packages/mobile-devtools/src/core/constants/index.ts b/packages/mobile-devtools/src/core/constants/index.ts index fc454b0..906aef7 100644 --- a/packages/mobile-devtools/src/core/constants/index.ts +++ b/packages/mobile-devtools/src/core/constants/index.ts @@ -19,6 +19,8 @@ export const LOG_LEVELS = { export const NETWORK_TYPES = { FETCH: 'fetch', XHR: 'xhr', + WEBSOCKET: 'websocket', + EVENTSOURCE: 'eventsource', } as const; export type NetworkRequestType = (typeof NETWORK_TYPES)[keyof typeof NETWORK_TYPES]; diff --git a/packages/mobile-devtools/src/core/index.ts b/packages/mobile-devtools/src/core/index.ts index 631dbaa..4a91f34 100644 --- a/packages/mobile-devtools/src/core/index.ts +++ b/packages/mobile-devtools/src/core/index.ts @@ -9,6 +9,8 @@ export * from './types/events'; export { DevToolsStore } from './stores/devtools-store'; export { ConsoleInterceptor } from './interceptors/console-interceptor'; export { NetworkInterceptor } from './interceptors/network-interceptor'; +export { WebSocketInterceptor } from './interceptors/websocket-interceptor'; +export { SSEInterceptor } from './interceptors/sse-interceptor'; export { StorageManager } from './managers/storage-manager'; export { SystemManager } from './managers/system-manager'; export { ElementsManager, STYLE_CATEGORIES } from './managers/elements-manager'; @@ -20,3 +22,6 @@ export * from './utils/env'; export * from './utils/bug-exporter'; export * from './utils/privacy'; export * from './utils/clipboard'; +export * from './utils/shake-detector'; +export * from './utils/id'; + diff --git a/packages/mobile-devtools/src/core/interceptors/__tests__/websocket-sse.test.ts b/packages/mobile-devtools/src/core/interceptors/__tests__/websocket-sse.test.ts new file mode 100644 index 0000000..0301853 --- /dev/null +++ b/packages/mobile-devtools/src/core/interceptors/__tests__/websocket-sse.test.ts @@ -0,0 +1,77 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { DevToolsStore } from '../../stores/devtools-store'; +import { SSEInterceptor } from '../sse-interceptor'; +import { WebSocketInterceptor } from '../websocket-interceptor'; + +describe('WebSocket & SSE Interceptors', () => { + let store: DevToolsStore; + let wsInterceptor: WebSocketInterceptor; + let sseInterceptor: SSEInterceptor; + + beforeEach(() => { + store = new DevToolsStore(); + wsInterceptor = new WebSocketInterceptor(store); + sseInterceptor = new SSEInterceptor(store); + }); + + afterEach(() => { + wsInterceptor.restore(); + sseInterceptor.restore(); + }); + + it('should intercept WebSocket connections and frame messages', () => { + const mockWS = vi.fn().mockImplementation(function (this: any) { + this.listeners = {} as Record; + this.addEventListener = (event: string, cb: Function) => { + if (!this.listeners[event]) this.listeners[event] = []; + this.listeners[event].push(cb); + }; + this.send = vi.fn(); + }); + (mockWS as any).CONNECTING = 0; + (mockWS as any).OPEN = 1; + (mockWS as any).CLOSING = 2; + (mockWS as any).CLOSED = 3; + + (globalThis as any).WebSocket = mockWS; + + wsInterceptor.init(); + + const ws = new (window.WebSocket as any)('ws://example.com/socket'); + const requests = store.getNetworkRequests(); + + expect(requests.length).toBe(1); + expect(requests[0]?.url).toBe('ws://example.com/socket'); + expect(requests[0]?.method).toBe('WS'); + + // Simulate sending data + ws.send('ping'); + expect(store.getNetworkRequests()[0]?.frames?.length).toBe(1); + expect(store.getNetworkRequests()[0]?.frames?.[0]?.type).toBe('sent'); + expect(store.getNetworkRequests()[0]?.frames?.[0]?.data).toBe('ping'); + }); + + it('should intercept EventSource connections', () => { + const mockES = vi.fn().mockImplementation(function (this: any) { + this.listeners = {} as Record; + this.addEventListener = (event: string, cb: Function) => { + if (!this.listeners[event]) this.listeners[event] = []; + this.listeners[event].push(cb); + }; + }); + (mockES as any).CONNECTING = 0; + (mockES as any).OPEN = 1; + (mockES as any).CLOSED = 2; + + (globalThis as any).EventSource = mockES; + + sseInterceptor.init(); + + new (window.EventSource as any)('http://example.com/sse'); + const requests = store.getNetworkRequests(); + + expect(requests.length).toBe(1); + expect(requests[0]?.url).toBe('http://example.com/sse'); + expect(requests[0]?.method).toBe('SSE'); + }); +}); diff --git a/packages/mobile-devtools/src/core/interceptors/console-interceptor.ts b/packages/mobile-devtools/src/core/interceptors/console-interceptor.ts index 90a7df4..34e2eed 100644 --- a/packages/mobile-devtools/src/core/interceptors/console-interceptor.ts +++ b/packages/mobile-devtools/src/core/interceptors/console-interceptor.ts @@ -1,6 +1,7 @@ import { DevToolsStore } from '../stores/devtools-store'; import { LogLevel } from '../types/log'; import { isServer } from '../utils/env'; +import { generateId } from '../utils/id'; export class ConsoleInterceptor { private originalMethods: Partial void>> = {}; @@ -66,7 +67,7 @@ export class ConsoleInterceptor { const processedArgs = args.map((arg) => this.serializeArg(arg)); this.store.addLog({ - id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + id: generateId('log'), level, args: processedArgs, timestamp: Date.now(), diff --git a/packages/mobile-devtools/src/core/interceptors/network-interceptor.ts b/packages/mobile-devtools/src/core/interceptors/network-interceptor.ts index 031e053..7b23463 100644 --- a/packages/mobile-devtools/src/core/interceptors/network-interceptor.ts +++ b/packages/mobile-devtools/src/core/interceptors/network-interceptor.ts @@ -2,6 +2,7 @@ import { NETWORK_STATUS, NETWORK_THROTTLING, NETWORK_TYPES } from '../constants' import { DevToolsStore } from '../stores/devtools-store'; import { NetworkRequestEntry } from '../types/network'; import { isServer } from '../utils/env'; +import { generateId } from '../utils/id'; import { maskSensitiveValue } from '../utils/privacy'; export class NetworkInterceptor { @@ -32,7 +33,7 @@ export class NetworkInterceptor { window.fetch = async (input: RequestInfo | URL, init?: RequestInit) => { const startTime = Date.now(); - const id = `${startTime}-${Math.random().toString(36).substring(2, 9)}`; + const id = generateId('req'); let url = ''; if (typeof input === 'string') { @@ -179,7 +180,7 @@ export class NetworkInterceptor { this.originalXhrSetRequestHeader = proto.setRequestHeader; proto.open = function (method: string, url: string | URL, ...rest: any[]) { - (this as any).__devToolsId = `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; + (this as any).__devToolsId = generateId('xhr'); (this as any).__devToolsMethod = method.toUpperCase(); (this as any).__devToolsUrl = typeof url === 'string' ? url : url.toString(); (this as any).__devToolsReqHeaders = {}; diff --git a/packages/mobile-devtools/src/core/interceptors/sse-interceptor.ts b/packages/mobile-devtools/src/core/interceptors/sse-interceptor.ts new file mode 100644 index 0000000..6e161e1 --- /dev/null +++ b/packages/mobile-devtools/src/core/interceptors/sse-interceptor.ts @@ -0,0 +1,88 @@ +import { NETWORK_TYPES } from '../constants'; +import { DevToolsStore } from '../stores/devtools-store'; +import { isBrowser } from '../utils/env'; +import { generateId } from '../utils/id'; + +export class SSEInterceptor { + private store: DevToolsStore; + private originalEventSource: typeof window.EventSource | null = null; + private isInitialized = false; + + constructor(store: DevToolsStore) { + this.store = store; + } + + public init() { + if (!isBrowser || this.isInitialized || typeof window.EventSource === 'undefined') return; + + this.originalEventSource = window.EventSource; + const store = this.store; + const OriginalEventSource = this.originalEventSource; + + function ProxyEventSource(this: any, url: string | URL, eventSourceInitDict?: EventSourceInit) { + const es = new (OriginalEventSource as any)(url, eventSourceInitDict); + const id = generateId('sse'); + const startTime = Date.now(); + + store.addNetworkRequest({ + id, + url: String(url), + method: 'SSE', + status: 200, + statusText: 'Connecting', + type: NETWORK_TYPES.EVENTSOURCE, + startTime, + frames: [], + }); + + es.addEventListener('open', () => { + store.updateNetworkRequest(id, { statusText: 'Open', status: 200 }); + }); + + es.addEventListener('message', (event: MessageEvent) => { + store.addNetworkFrame(id, { + id: generateId('f'), + type: 'received', + data: event.data, + timestamp: Date.now(), + }); + }); + + const originalAddEventListener = es.addEventListener.bind(es); + es.addEventListener = function (type: string, listener: any, options?: any) { + if (type !== 'open' && type !== 'error' && type !== 'message') { + originalAddEventListener(type, (event: MessageEvent) => { + store.addNetworkFrame(id, { + id: generateId('f'), + type: 'received', + data: event?.data !== undefined ? event.data : String(event), + timestamp: Date.now(), + }); + }); + } + return originalAddEventListener(type, listener, options); + }; + + es.addEventListener('error', () => { + store.updateNetworkRequest(id, { statusText: 'Error', errorState: 'error' }); + }); + + return es; + } + + ProxyEventSource.prototype = OriginalEventSource.prototype; + ProxyEventSource.CONNECTING = OriginalEventSource.CONNECTING; + ProxyEventSource.OPEN = OriginalEventSource.OPEN; + ProxyEventSource.CLOSED = OriginalEventSource.CLOSED; + + window.EventSource = ProxyEventSource as any; + this.isInitialized = true; + } + + public restore() { + if (!isBrowser || !this.isInitialized || !this.originalEventSource) return; + window.EventSource = this.originalEventSource; + this.originalEventSource = null; + this.isInitialized = false; + } +} diff --git a/packages/mobile-devtools/src/core/interceptors/websocket-interceptor.ts b/packages/mobile-devtools/src/core/interceptors/websocket-interceptor.ts new file mode 100644 index 0000000..4e0f982 --- /dev/null +++ b/packages/mobile-devtools/src/core/interceptors/websocket-interceptor.ts @@ -0,0 +1,95 @@ +import { NETWORK_TYPES } from '../constants'; +import { DevToolsStore } from '../stores/devtools-store'; +import { isBrowser } from '../utils/env'; +import { generateId } from '../utils/id'; + +export class WebSocketInterceptor { + private store: DevToolsStore; + private originalWebSocket: typeof window.WebSocket | null = null; + private isInitialized = false; + + constructor(store: DevToolsStore) { + this.store = store; + } + + public init() { + if (!isBrowser || this.isInitialized || typeof window.WebSocket === 'undefined') return; + + this.originalWebSocket = window.WebSocket; + const store = this.store; + const OriginalWebSocket = this.originalWebSocket; + + function ProxyWebSocket(this: any, url: string | URL, protocols?: string | string[]) { + const ws = new (OriginalWebSocket as any)(url, protocols); + const id = generateId('ws'); + const startTime = Date.now(); + + store.addNetworkRequest({ + id, + url: String(url), + method: 'WS', + status: 101, + statusText: 'Connecting', + type: NETWORK_TYPES.WEBSOCKET, + startTime, + requestHeaders: protocols ? { 'Sec-WebSocket-Protocol': Array.isArray(protocols) ? protocols.join(', ') : String(protocols) } : undefined, + frames: [], + }); + + ws.addEventListener('open', () => { + store.updateNetworkRequest(id, { statusText: 'Open', status: 101 }); + }); + + ws.addEventListener('message', (event: MessageEvent) => { + store.addNetworkFrame(id, { + id: generateId('f'), + type: 'received', + data: event.data, + timestamp: Date.now(), + }); + }); + + ws.addEventListener('error', () => { + store.updateNetworkRequest(id, { statusText: 'Error', errorState: 'error' }); + }); + + ws.addEventListener('close', (event: CloseEvent) => { + const endTime = Date.now(); + store.updateNetworkRequest(id, { + statusText: `Closed (${event.code})`, + endTime, + duration: endTime - startTime, + }); + }); + + const originalSend = ws.send.bind(ws); + ws.send = function (data: any) { + store.addNetworkFrame(id, { + id: generateId('f'), + type: 'sent', + data: typeof data === 'object' ? JSON.stringify(data) : String(data), + timestamp: Date.now(), + }); + return originalSend(data); + }; + + return ws; + } + + ProxyWebSocket.prototype = OriginalWebSocket.prototype; + ProxyWebSocket.CONNECTING = OriginalWebSocket.CONNECTING; + ProxyWebSocket.OPEN = OriginalWebSocket.OPEN; + ProxyWebSocket.CLOSING = OriginalWebSocket.CLOSING; + ProxyWebSocket.CLOSED = OriginalWebSocket.CLOSED; + + window.WebSocket = ProxyWebSocket as any; + this.isInitialized = true; + } + + public restore() { + if (!isBrowser || !this.isInitialized || !this.originalWebSocket) return; + window.WebSocket = this.originalWebSocket; + this.originalWebSocket = null; + this.isInitialized = false; + } +} diff --git a/packages/mobile-devtools/src/core/stores/devtools-store.ts b/packages/mobile-devtools/src/core/stores/devtools-store.ts index 378ef75..a418965 100644 --- a/packages/mobile-devtools/src/core/stores/devtools-store.ts +++ b/packages/mobile-devtools/src/core/stores/devtools-store.ts @@ -288,9 +288,24 @@ export class DevToolsStore { }; this.networkRequests[index] = updated; - if (!this.isOpen && (updated.status >= 400 || updated.errorState === 'error')) { + if (!this.isOpen && updates.status && updates.status >= 400) { this.unreadErrorCount++; } + + this.notify(); + } + } + + public addNetworkFrame(id: string, frame: import('../types/network').NetworkFrameMessage) { + const index = this.networkRequests.findIndex((r) => r.id === id); + if (index !== -1 && this.networkRequests[index]) { + const existing = this.networkRequests[index]; + const frames = [...(existing.frames || []), frame]; + if (frames.length > 200) frames.shift(); + this.networkRequests[index] = { + ...existing, + frames, + }; this.notify(); } } diff --git a/packages/mobile-devtools/src/core/types/config.ts b/packages/mobile-devtools/src/core/types/config.ts index 63a6c92..e182ec6 100644 --- a/packages/mobile-devtools/src/core/types/config.ts +++ b/packages/mobile-devtools/src/core/types/config.ts @@ -1,72 +1,228 @@ import { BadgePositionPreset, ThemeMode } from '../constants'; +/** + * Built-in tab identifiers or custom tab ID strings. + */ export type DevToolsTabId = 'console' | 'elements' | 'network' | 'storage' | 'system' | string; +/** + * Position coordinates or corner preset for the floating badge. + */ export interface BadgePosition { + /** X-coordinate in pixels from left edge of screen */ x: number; + /** Y-coordinate in pixels from top edge of screen */ y: number; + /** Optional corner preset for initial placement */ corner?: BadgePositionPreset; } export type { ThemeMode }; +/** + * Theme customization options for Mobile DevTools UI overlay. + */ export interface DevToolsTheme { + /** + * Theme mode setting ('dark', 'light', or 'auto'). + * @default 'dark' + */ mode?: ThemeMode; + /** Primary background color CSS value */ backgroundColor?: string; + /** Card background color CSS value */ cardBackgroundColor?: string; + /** Card border color CSS value */ cardBorderColor?: string; + /** Primary border color CSS value */ borderColor?: string; + /** Main text color CSS value */ textColor?: string; + /** Muted/secondary text color CSS value */ textMutedColor?: string; + /** Accent brand color CSS value */ accentColor?: string; + /** Error indicator color CSS value */ errorColor?: string; + /** Warning indicator color CSS value */ warningColor?: string; + /** Success indicator color CSS value */ successColor?: string; + /** Font family stack for DevTools overlay */ fontFamily?: string; } +/** + * Configuration options for Network, Console, WebSocket, and SSE interceptors. + */ export interface InterceptorConfig { + /** + * Maximum number of console logs retained in memory. + * @default 200 + */ maxLogLimit?: number; + /** + * Maximum number of network requests retained in memory. + * @default 100 + */ maxNetworkLimit?: number; + /** + * List of URL strings or RegExps to exclude from network interception. + * @example ['/healthz', /\.png$/] + */ ignoreNetworkUrls?: (string | RegExp)[]; + /** + * Enable or disable console log/warn/error interception. + * @default true + */ enableConsoleInterceptor?: boolean; + /** + * Enable or disable window.fetch API interception. + * @default true + */ enableFetchInterceptor?: boolean; + /** + * Enable or disable XMLHttpRequest interception. + * @default true + */ enableXhrInterceptor?: boolean; + /** + * Enable or disable WebSocket frame interception. + * @default true + */ + enableWebSocketInterceptor?: boolean; + /** + * Enable or disable EventSource (SSE) stream interception. + * @default true + */ + enableSSEInterceptor?: boolean; } +/** + * Definition for registering custom tabs in Mobile DevTools. + */ export interface CustomTabDefinition { + /** Unique ID for the custom tab */ id: string; + /** Display title shown in the tab header */ title: string; + /** Optional SVG icon string */ icon?: string; + /** Custom render function executed when the tab is active */ render?: (container: HTMLElement) => void; } +/** + * Privacy and sensitive data masking configuration. + */ export interface PrivacyConfig { + /** + * Keys or header names to mask in DevTools views (e.g. passwords, tokens). + * @example ['password', 'authorization', 'secret'] + */ mask?: string[]; } +/** + * Custom CSS style overrides for DevTools DOM elements. + */ export interface DevToolsStyles { + /** Style overrides for the floating trigger badge */ badge?: Record; + /** Style overrides for the main drawer panel */ drawer?: Record; + /** Style overrides for the backdrop overlay */ overlay?: Record; + /** Style overrides for drag handle elements */ handle?: Record; } +/** + * Main Configuration interface for Mobile DevTools overlay. + */ export interface DevToolsConfig { + /** + * Enable or disable DevTools overlay. + * @default true + */ enabled?: boolean; + /** + * Force DevTools to mount regardless of NODE_ENV environment. + * @default false + */ forceEnable?: boolean; + /** + * Open DevTools drawer immediately upon mounting. + * @default false + */ defaultOpen?: boolean; + /** + * Allow physical device shake motion gesture to toggle DevTools drawer. + * @default true + */ + shakeToToggle?: boolean; + /** + * Acceleration threshold required to trigger device shake toggle. + * @default 12 + */ + shakeThreshold?: number; + /** + * Show or hide the floating badge trigger on screen. + * @default true + */ + showBadge?: boolean; + /** + * Tab to display initially when DevTools is opened. + * @default 'console' + */ initialTab?: DevToolsTabId; + /** + * Array of enabled built-in tab IDs to display in DevTools drawer. + * @default ['console', 'elements', 'network', 'storage', 'system'] + */ enabledTabs?: DevToolsTabId[]; + /** + * Custom tabs registered to extend DevTools functionality. + */ customTabs?: CustomTabDefinition[]; + /** + * Theme configuration (mode, colors, font family). + */ theme?: DevToolsTheme; + /** + * Interceptor options for logs, network requests, WebSockets, and SSE. + */ interceptors?: InterceptorConfig; + /** + * Sensitive data masking rules. + */ privacy?: PrivacyConfig; + /** + * Initial screen corner or coordinates for the floating badge. + * @default 'bottom-right' + */ position?: BadgePosition | BadgePositionPreset; + /** + * Custom CSS style overrides for DevTools overlay elements. + */ styles?: DevToolsStyles; - badgeStyle?: Record; + /** + * Title text displayed in the DevTools drawer header. + * @default 'DevTools' + */ title?: string; + /** + * Custom logo/icon SVG displayed in DevTools header. + */ icon?: any; + /** + * Automatically snap floating badge to nearest screen edge when dragged. + * @default false + */ autoSnapBadge?: boolean; + /** + * Parent HTML element to mount Shadow DOM container into. + * @default document.body + */ container?: HTMLElement | null; } diff --git a/packages/mobile-devtools/src/core/types/network.ts b/packages/mobile-devtools/src/core/types/network.ts index fe6db79..ecf67f3 100644 --- a/packages/mobile-devtools/src/core/types/network.ts +++ b/packages/mobile-devtools/src/core/types/network.ts @@ -6,6 +6,13 @@ import { export type { NetworkRequestType, NetworkStatus, NetworkThrottlingProfile }; +export interface NetworkFrameMessage { + id: string; + type: 'sent' | 'received'; + data: any; + timestamp: number; +} + export interface NetworkRequestEntry { id: string; url: string; @@ -22,4 +29,5 @@ export interface NetworkRequestEntry { responseBody?: any; responseType?: string; errorState?: NetworkStatus; + frames?: NetworkFrameMessage[]; } diff --git a/packages/mobile-devtools/src/core/utils/__tests__/formatters.test.ts b/packages/mobile-devtools/src/core/utils/__tests__/formatters.test.ts index fc1c9c0..543ba50 100644 --- a/packages/mobile-devtools/src/core/utils/__tests__/formatters.test.ts +++ b/packages/mobile-devtools/src/core/utils/__tests__/formatters.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect } from 'vitest'; import { + formatCount, formatDuration, formatTimestamp, generateCurlCommand, @@ -8,6 +9,18 @@ import { import { NetworkRequestEntry } from '../../types/network'; describe('formatters utils', () => { + it('should format counters with max 99+ ceiling', () => { + expect(formatCount(0)).toBe('0'); + expect(formatCount(-5)).toBe('0'); + expect(formatCount(5)).toBe('5'); + expect(formatCount(99)).toBe('99'); + expect(formatCount(100)).toBe('99+'); + expect(formatCount(1000)).toBe('99+'); + + // Custom limit + expect(formatCount(500, 999)).toBe('500'); + expect(formatCount(1000, 999)).toBe('999+'); + }); it('should format durations properly', () => { expect(formatDuration(50)).toBe('50ms'); expect(formatDuration(1500)).toBe('1.50s'); diff --git a/packages/mobile-devtools/src/core/utils/__tests__/id.test.ts b/packages/mobile-devtools/src/core/utils/__tests__/id.test.ts new file mode 100644 index 0000000..bf622fc --- /dev/null +++ b/packages/mobile-devtools/src/core/utils/__tests__/id.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it, vi } from 'vitest'; +import { generateId } from '../id'; + +describe('generateId', () => { + it('should generate an ID with default prefix when none is provided', () => { + const id = generateId(); + expect(id).toBeDefined(); + expect(typeof id).toBe('string'); + expect(id.startsWith('id_')).toBe(true); + }); + + it('should generate an ID with custom prefix', () => { + const reqId = generateId('req'); + expect(reqId.startsWith('req_')).toBe(true); + + const wsId = generateId('ws'); + expect(wsId.startsWith('ws_')).toBe(true); + + const logId = generateId('log'); + expect(logId.startsWith('log_')).toBe(true); + }); + + it('should generate 1000 unique IDs without collision', () => { + const ids = new Set(); + const count = 1000; + + for (let i = 0; i < count; i++) { + ids.add(generateId('test')); + } + + expect(ids.size).toBe(count); + }); + + it('should fallback gracefully if crypto.randomUUID is not available', () => { + const originalCrypto = globalThis.crypto; + + // Temporarily mock environment without crypto.randomUUID + Object.defineProperty(globalThis, 'crypto', { + value: undefined, + configurable: true, + }); + + const fallbackId = generateId('fallback'); + expect(fallbackId).toBeDefined(); + expect(fallbackId.startsWith('fallback_')).toBe(true); + + // Restore original crypto + Object.defineProperty(globalThis, 'crypto', { + value: originalCrypto, + configurable: true, + }); + }); +}); diff --git a/packages/mobile-devtools/src/core/utils/__tests__/shake-detector.test.ts b/packages/mobile-devtools/src/core/utils/__tests__/shake-detector.test.ts new file mode 100644 index 0000000..49d9148 --- /dev/null +++ b/packages/mobile-devtools/src/core/utils/__tests__/shake-detector.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it, vi } from 'vitest'; +import { ShakeDetector } from '../shake-detector'; + +describe('ShakeDetector', () => { + it('should trigger onShake when linear motion magnitude exceeds threshold', () => { + const onShake = vi.fn(); + const detector = new ShakeDetector({ threshold: 10, cooldown: 500, onShake }); + + // Initial motion event (establishes baseline gravity: 0, 0, 9.8) + detector.handleMotion({ + accelerationIncludingGravity: { x: 0, y: 0, z: 9.8 }, + } as any); + + expect(onShake).not.toHaveBeenCalled(); + + // High dynamic acceleration event (linear motion spike) + detector.handleMotion({ + accelerationIncludingGravity: { x: 15, y: 15, z: 25 }, + } as any); + + expect(onShake).toHaveBeenCalledTimes(1); + }); + + it('should respect cooldown period between shakes', () => { + const onShake = vi.fn(); + const detector = new ShakeDetector({ threshold: 5, cooldown: 1000, onShake }); + + detector.handleMotion({ + accelerationIncludingGravity: { x: 0, y: 0, z: 0 }, + } as any); + + detector.handleMotion({ + accelerationIncludingGravity: { x: 10, y: 10, z: 10 }, + } as any); + + expect(onShake).toHaveBeenCalledTimes(1); + + // Immediate second shake within cooldown window + detector.handleMotion({ + accelerationIncludingGravity: { x: 20, y: 20, z: 20 }, + } as any); + + expect(onShake).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/mobile-devtools/src/core/utils/bug-exporter.ts b/packages/mobile-devtools/src/core/utils/bug-exporter.ts index a352e62..b1d488b 100644 --- a/packages/mobile-devtools/src/core/utils/bug-exporter.ts +++ b/packages/mobile-devtools/src/core/utils/bug-exporter.ts @@ -2,12 +2,23 @@ import { DevToolsStore } from '../stores/devtools-store'; import { copyToClipboard } from './clipboard'; import { isBrowser } from './env'; +/** + * Result metrics returned after running a bug report export operation. + */ export interface BugReportResult { + /** True if native OS Web Share API dialog was successfully invoked */ shared: boolean; + /** True if a file download trigger was executed */ downloaded: boolean; + /** True if the bug report text was successfully copied to clipboard */ copied: boolean; } +/** + * Generates a clean plain text report containing system specs, captured console logs, and network requests. + * @param store DevTools store instance. + * @returns Plain text formatted bug report. + */ export function generateBugReportText(store: DevToolsStore): string { const config = store.getConfig(); const logs = store.getLogs(); @@ -88,6 +99,11 @@ NETWORK SUMMARY (${network.length} Total | ${failedReqCount} Failed) return text; } +/** + * Triggers native mobile share dialog (Web Share API) or fallback file download + clipboard copy. + * @param store DevTools store instance. + * @returns Result status object. + */ export async function exportBugReport(store: DevToolsStore): Promise { const reportText = generateBugReportText(store); const fileName = `bug-report-${Date.now()}.txt`; diff --git a/packages/mobile-devtools/src/core/utils/drag-helper.ts b/packages/mobile-devtools/src/core/utils/drag-helper.ts index 1ce4c7a..4fca388 100644 --- a/packages/mobile-devtools/src/core/utils/drag-helper.ts +++ b/packages/mobile-devtools/src/core/utils/drag-helper.ts @@ -1,11 +1,17 @@ import { BADGE_POSITIONS, BadgePositionPreset } from '../constants'; import { isBrowser, isServer } from './env'; +/** + * 2D Screen coordinates for dragging elements. + */ export interface DragPosition { x: number; y: number; } +/** + * Viewport boundaries for constraining drag interactions. + */ export interface DragBounds { minX: number; maxX: number; @@ -13,6 +19,14 @@ export interface DragBounds { maxY: number; } +/** + * Clamps coordinates within the visible screen viewport. + * @param pos Current drag position. + * @param elementWidth Width of element. + * @param elementHeight Height of element. + * @param margin Minimum edge margin. + * @returns Clamped DragPosition coordinates. + */ export function clampPositionToViewport( pos: DragPosition, elementWidth = 125, @@ -32,6 +46,16 @@ export function clampPositionToViewport( }; } +/** + * Calculates snapped X/Y position to snap floating badge to the left or right screen edge. + * @param currentPos Current drag position. + * @param elementWidth Element width. + * @param elementHeight Element height. + * @param viewportWidth Screen viewport width. + * @param viewportHeight Screen viewport height. + * @param margin Edge padding margin. + * @returns Snapped DragPosition coordinates. + */ export function calculateSnapPosition( currentPos: DragPosition, elementWidth: number, @@ -55,6 +79,14 @@ export function calculateSnapPosition( }; } +/** + * Computes default screen pixel coordinates based on a position preset (e.g. 'bottom-right'). + * @param positionPreset Position preset string or custom coordinates. + * @param badgeWidth Badge element width. + * @param badgeHeight Badge element height. + * @param margin Edge margin padding. + * @returns Calculated DragPosition. + */ export function getDefaultPosition( positionPreset: BadgePositionPreset | DragPosition = BADGE_POSITIONS.BOTTOM_RIGHT, badgeWidth = 125, diff --git a/packages/mobile-devtools/src/core/utils/env.ts b/packages/mobile-devtools/src/core/utils/env.ts index 73d1e94..aa79fef 100644 --- a/packages/mobile-devtools/src/core/utils/env.ts +++ b/packages/mobile-devtools/src/core/utils/env.ts @@ -1,2 +1,9 @@ +/** + * True if running in a client-side browser environment with access to `window` and `document`. + */ export const isBrowser = typeof window !== 'undefined'; + +/** + * True if running in a server-side environment (Node.js, SSR, Workers). + */ export const isServer = !isBrowser; diff --git a/packages/mobile-devtools/src/core/utils/formatters.ts b/packages/mobile-devtools/src/core/utils/formatters.ts index a9bbf85..2a48cf4 100644 --- a/packages/mobile-devtools/src/core/utils/formatters.ts +++ b/packages/mobile-devtools/src/core/utils/formatters.ts @@ -1,6 +1,11 @@ import { NetworkRequestEntry } from '../types/network'; import { isServer } from './env'; +/** + * Formats epoch millisecond timestamp into HH:MM:SS.mmm format. + * @param time Epoch timestamp in milliseconds. + * @returns Formatted time string, e.g. "14:32:05.120". + */ export function formatTimestamp(time: number): string { const date = new Date(time); const hours = date.getHours().toString().padStart(2, '0'); @@ -10,12 +15,33 @@ export function formatTimestamp(time: number): string { return `${hours}:${minutes}:${seconds}.${ms}`; } +/** + * Formats duration milliseconds into human-readable ms or seconds. + * @param durationMs Duration in milliseconds. + * @returns Formatted duration string, e.g. "45ms", "1.25s", or "pending". + */ export function formatDuration(durationMs?: number): string { if (durationMs === undefined) return 'pending'; if (durationMs < 1000) return `${Math.round(durationMs)}ms`; return `${(durationMs / 1000).toFixed(2)}s`; } +/** + * Formats numeric count badges with a maximum cap. + * @param count Numeric count value. + * @param max Maximum display threshold (default: 99). + * @returns Formatted string count, e.g. "5", "99+", or "0". + */ +export function formatCount(count: number, max = 99): string { + if (typeof count !== 'number' || count <= 0) return '0'; + return count > max ? `${max}+` : String(count); +} + +/** + * Generates an executable cURL CLI command string from a captured network request entry. + * @param req Network request entry. + * @returns Formatted multi-line cURL command string. + */ export function generateCurlCommand(req: NetworkRequestEntry): string { let curl = `curl -X ${req.method.toUpperCase()} "${req.url}"`; @@ -35,6 +61,11 @@ export function generateCurlCommand(req: NetworkRequestEntry): string { return curl; } +/** + * Internal helper to format request/response body payloads into formatted JSON or string summaries. + * @param body Raw body payload (object, string, or null). + * @returns Formatted summary string or 'None'. + */ function formatBodySummary(body: unknown): string { if (body === undefined || body === null || body === '') { return 'None'; @@ -45,6 +76,11 @@ function formatBodySummary(body: unknown): string { return String(body); } +/** + * Generates a complete text summary report of a captured HTTP request and response. + * @param req Network request entry. + * @returns Formatted plain text request/response summary. + */ export function generateFullRequestSummary(req: NetworkRequestEntry): string { const payloadStr = formatBodySummary(req.requestBody); const responseStr = formatBodySummary(req.responseBody); @@ -90,6 +126,10 @@ ${responseStr} `; } +/** + * Parses current document.cookie string into a key-value object map. + * @returns Map of cookie key names to value strings. + */ export function parseCookies(): Record { const cookies: Record = {}; if (isServer || !document.cookie) return cookies; diff --git a/packages/mobile-devtools/src/core/utils/id.ts b/packages/mobile-devtools/src/core/utils/id.ts new file mode 100644 index 0000000..12ed810 --- /dev/null +++ b/packages/mobile-devtools/src/core/utils/id.ts @@ -0,0 +1,18 @@ +import { isBrowser } from './env'; + +/** + * Generates a unique string ID using crypto.randomUUID() with a fallback. + * @param prefix Optional prefix string (e.g. 'req', 'ws', 'log', 'f') + */ +export function generateId(prefix = 'id'): string { + if ( + isBrowser && + typeof crypto !== 'undefined' && + typeof crypto.randomUUID === 'function' + ) { + return `${prefix}_${crypto.randomUUID().substring(0, 8)}`; + } + + // Fallback for legacy environments + return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 7)}`; +} diff --git a/packages/mobile-devtools/src/core/utils/privacy.ts b/packages/mobile-devtools/src/core/utils/privacy.ts index 74b3704..77ab8c6 100644 --- a/packages/mobile-devtools/src/core/utils/privacy.ts +++ b/packages/mobile-devtools/src/core/utils/privacy.ts @@ -1,3 +1,10 @@ +/** + * Recursively masks sensitive key values (e.g. passwords, auth tokens) in objects or primitive strings. + * @param value Target payload or object. + * @param keyName Optional property key name. + * @param maskKeys Array of key substrings to mask (case-insensitive). + * @returns Masked object payload or string "****** (Masked)". + */ export function maskSensitiveValue( value: any, keyName = '', diff --git a/packages/mobile-devtools/src/core/utils/shake-detector.ts b/packages/mobile-devtools/src/core/utils/shake-detector.ts new file mode 100644 index 0000000..f2441f7 --- /dev/null +++ b/packages/mobile-devtools/src/core/utils/shake-detector.ts @@ -0,0 +1,152 @@ +import { isBrowser } from './env'; + +/** + * Options for configuring physical device shake gesture detector. + */ +export interface ShakeDetectorOptions { + /** Acceleration magnitude threshold in m/sยฒ (default: 12) */ + threshold?: number; + /** Minimum time between triggered shake events in ms (default: 800) */ + cooldown?: number; + /** Callback function executed when device shake is detected */ + onShake: () => void; +} + +/** + * Physical device shake gesture detector using low-pass gravity filter sensor fusion. + */ +export class ShakeDetector { + private threshold: number; + private cooldown: number; + private onShake: () => void; + private lastShakeTime = 0; + private isListening = false; + private gestureListenersBound = false; + + // Low-pass filter gravity components + private gravityX = 0; + private gravityY = 0; + private gravityZ = 0; + private initialized = false; + + private handleMotionBound = this.handleMotion.bind(this); + private handleGestureBound = this.handleGesturePermission.bind(this); + + constructor(options: ShakeDetectorOptions) { + this.threshold = options.threshold ?? 12; + this.cooldown = options.cooldown ?? 800; + this.onShake = options.onShake; + } + + /** + * Starts listening to device motion sensors and handles iOS permission requests. + */ + public start() { + if (!isBrowser || this.isListening) return; + + const DeviceMotionEventClass = window.DeviceMotionEvent as any; + if (typeof DeviceMotionEventClass === 'undefined') return; + + // Immediately bind devicemotion listener so shake works right away on initial page load + this.bindListener(); + + // Check iOS 13+ permission API to ensure explicit permission request on first interaction if needed + if (typeof DeviceMotionEventClass.requestPermission === 'function') { + if (!this.gestureListenersBound) { + window.addEventListener('touchstart', this.handleGestureBound, { once: true, capture: true }); + window.addEventListener('touchend', this.handleGestureBound, { once: true, capture: true }); + window.addEventListener('click', this.handleGestureBound, { once: true, capture: true }); + window.addEventListener('pointerup', this.handleGestureBound, { once: true, capture: true }); + this.gestureListenersBound = true; + } + } + } + + private handleGesturePermission() { + const DeviceMotionEventClass = window.DeviceMotionEvent as any; + if ( + typeof DeviceMotionEventClass !== 'undefined' && + typeof DeviceMotionEventClass.requestPermission === 'function' + ) { + // Synchronous call inside user gesture call stack required by iOS Safari + DeviceMotionEventClass.requestPermission() + .then((permissionState: string) => { + if (permissionState === 'granted') { + this.bindListener(); + } + }) + .catch((err: any) => { + console.warn('[DevTools] DeviceMotion permission request rejected:', err); + }); + } + } + + private bindListener() { + if (this.isListening) return; + if (typeof window.DeviceMotionEvent !== 'undefined') { + window.addEventListener('devicemotion', this.handleMotionBound, false); + this.isListening = true; + } + } + + /** + * Stops sensor motion listeners and cleans up iOS gesture event listeners. + */ + public stop() { + if (!isBrowser) return; + + if (typeof window.DeviceMotionEvent !== 'undefined') { + window.removeEventListener('devicemotion', this.handleMotionBound, false); + } + window.removeEventListener('touchstart', this.handleGestureBound, { capture: true }); + window.removeEventListener('touchend', this.handleGestureBound, { capture: true }); + window.removeEventListener('click', this.handleGestureBound, { capture: true }); + window.removeEventListener('pointerup', this.handleGestureBound, { capture: true }); + + this.isListening = false; + this.gestureListenersBound = false; + this.initialized = false; + this.gravityX = 0; + this.gravityY = 0; + this.gravityZ = 0; + } + + /** + * Handles device motion sensor input events and calculates dynamic user linear acceleration. + * @param event DeviceMotionEvent instance. + */ + public handleMotion(event: DeviceMotionEvent) { + const accel = event.accelerationIncludingGravity || event.acceleration; + if (!accel) return; + + const { x, y, z } = accel; + if (x === null || y === null || z === null) return; + + if (!this.initialized) { + this.gravityX = x; + this.gravityY = y; + this.gravityZ = z; + this.initialized = true; + return; + } + + // Low-pass filter (alpha = 0.8) to isolate static gravity vector + const alpha = 0.8; + this.gravityX = alpha * this.gravityX + (1 - alpha) * x; + this.gravityY = alpha * this.gravityY + (1 - alpha) * y; + this.gravityZ = alpha * this.gravityZ + (1 - alpha) * z; + + // Linear acceleration (dynamic user motion with gravity removed) + const linearX = x - this.gravityX; + const linearY = y - this.gravityY; + const linearZ = z - this.gravityZ; + + const motionMagnitude = Math.sqrt(linearX * linearX + linearY * linearY + linearZ * linearZ); + const now = Date.now(); + + if (motionMagnitude >= this.threshold && now - this.lastShakeTime > this.cooldown) { + this.lastShakeTime = now; + this.onShake(); + } + } +} diff --git a/packages/mobile-devtools/src/svelte.ts b/packages/mobile-devtools/src/svelte.ts new file mode 100644 index 0000000..0d03c12 --- /dev/null +++ b/packages/mobile-devtools/src/svelte.ts @@ -0,0 +1 @@ +export * from './adapters/svelte'; diff --git a/packages/mobile-devtools/src/ui/components/json-tree.ts b/packages/mobile-devtools/src/ui/components/json-tree.ts index 3f4464c..0c5338b 100644 --- a/packages/mobile-devtools/src/ui/components/json-tree.ts +++ b/packages/mobile-devtools/src/ui/components/json-tree.ts @@ -1,3 +1,5 @@ +import { formatCount } from '../../core'; + export function renderJsonTree(data: any, indentLevel = 0): HTMLElement { const container = document.createElement('div'); container.className = 'devtools-json-container'; @@ -51,7 +53,7 @@ export function renderJsonTree(data: any, indentLevel = 0): HTMLElement { const label = document.createElement('span'); label.className = 'devtools-json-key'; - label.textContent = isArray ? `Array(${count})` : `Object{${count}}`; + label.textContent = isArray ? `Array(${formatCount(count)})` : `Object{${formatCount(count)}}`; toggle.appendChild(arrow); toggle.appendChild(label); diff --git a/packages/mobile-devtools/src/ui/index.ts b/packages/mobile-devtools/src/ui/index.ts index eee65b5..98a59db 100644 --- a/packages/mobile-devtools/src/ui/index.ts +++ b/packages/mobile-devtools/src/ui/index.ts @@ -3,11 +3,12 @@ import { DEVTOOLS_CLASSNAMES, DevToolsConfig, DevToolsStore, - ElementsManager, isBrowser, NetworkInterceptor, ShadowHostManager, - StorageManager, + ShakeDetector, + SSEInterceptor, + WebSocketInterceptor, } from '../core'; import { DrawerView } from './views/drawer'; import { FloatingBadgeView } from './views/floating-badge'; @@ -24,6 +25,9 @@ export class MobileDevToolsEngine { private shadowContainer: HTMLElement | null = null; private consoleInterceptor: ConsoleInterceptor | null = null; private networkInterceptor: NetworkInterceptor | null = null; + private wsInterceptor: WebSocketInterceptor | null = null; + private sseInterceptor: SSEInterceptor | null = null; + private shakeDetector: ShakeDetector | null = null; private badgeView: FloatingBadgeView | null = null; private drawerView: DrawerView | null = null; private unsubscribeStore: (() => void) | null = null; @@ -86,6 +90,26 @@ export class MobileDevToolsEngine { this.networkInterceptor.init(); } + if (interceptorsConfig?.enableWebSocketInterceptor !== false) { + this.wsInterceptor = new WebSocketInterceptor(this.store); + this.wsInterceptor.init(); + } + + if (interceptorsConfig?.enableSSEInterceptor !== false) { + this.sseInterceptor = new SSEInterceptor(this.store); + this.sseInterceptor.init(); + } + + if (config.shakeToToggle) { + this.shakeDetector = new ShakeDetector({ + threshold: config.shakeThreshold ?? 12, + onShake: () => { + this.store.toggleOpen(); + }, + }); + this.shakeDetector.start(); + } + // Render Floating Badge & Drawer UI Elements into Shadow DOM Container this.badgeView = new FloatingBadgeView(this.store); const badgeElement = this.badgeView.render(); @@ -110,6 +134,10 @@ export class MobileDevToolsEngine { } public destroy() { + if (this.shakeDetector) { + this.shakeDetector.stop(); + this.shakeDetector = null; + } if (this.badgeView) { this.badgeView.destroy(); this.badgeView = null; @@ -130,6 +158,14 @@ export class MobileDevToolsEngine { this.networkInterceptor.restore(); this.networkInterceptor = null; } + if (this.wsInterceptor) { + this.wsInterceptor.restore(); + this.wsInterceptor = null; + } + if (this.sseInterceptor) { + this.sseInterceptor.restore(); + this.sseInterceptor = null; + } if (this.shadowHostManager) { this.shadowHostManager.unmount(); this.shadowHostManager = null; diff --git a/packages/mobile-devtools/src/ui/styles/shadow-styles.ts b/packages/mobile-devtools/src/ui/styles/shadow-styles.ts index 6091c61..72ca9c3 100644 --- a/packages/mobile-devtools/src/ui/styles/shadow-styles.ts +++ b/packages/mobile-devtools/src/ui/styles/shadow-styles.ts @@ -487,6 +487,8 @@ export const SHADOW_STYLES = ` border-bottom: 1px solid var(--dev-border); gap: 6px; overflow-x: auto; + -webkit-overflow-scrolling: touch; + touch-action: pan-x pan-y; } .devtools-tab-btn { diff --git a/packages/mobile-devtools/src/ui/utils/__tests__/scroll-lock.test.ts b/packages/mobile-devtools/src/ui/utils/__tests__/scroll-lock.test.ts index 0737b63..b963113 100644 --- a/packages/mobile-devtools/src/ui/utils/__tests__/scroll-lock.test.ts +++ b/packages/mobile-devtools/src/ui/utils/__tests__/scroll-lock.test.ts @@ -7,10 +7,10 @@ describe('scroll-lock', () => { setupScrollLockGuard(el); expect(el.style.overscrollBehavior).toBe('contain'); - expect(el.style.touchAction).toBe('pan-y'); + expect(el.style.touchAction).toBe('pan-x pan-y'); }); - it('should adjust scrollTop to 1px on touchstart when scrollTop is 0 and scrollable', () => { + it('should adjust scrollTop to 1px on touchstart when scrollTop is 0 and vertically scrollable', () => { const el = document.createElement('div'); Object.defineProperty(el, 'clientHeight', { value: 100, configurable: true }); Object.defineProperty(el, 'scrollHeight', { value: 300, configurable: true }); @@ -24,6 +24,20 @@ describe('scroll-lock', () => { expect(el.scrollTop).toBe(1); }); + it('should adjust scrollLeft to 1px on touchstart when scrollLeft is 0 and horizontally scrollable', () => { + const el = document.createElement('div'); + Object.defineProperty(el, 'clientWidth', { value: 100, configurable: true }); + Object.defineProperty(el, 'scrollWidth', { value: 300, configurable: true }); + el.scrollLeft = 0; + + setupScrollLockGuard(el); + + const touchEvent = new Event('touchstart', { bubbles: true }); + el.dispatchEvent(touchEvent); + + expect(el.scrollLeft).toBe(1); + }); + it('should stopPropagation on touchmove', () => { const el = document.createElement('div'); Object.defineProperty(el, 'clientHeight', { value: 100, configurable: true }); @@ -39,10 +53,12 @@ describe('scroll-lock', () => { expect(stopPropagationSpy).toHaveBeenCalled(); }); - it('should preventDefault on touchmove if content is not scrollable', () => { + it('should preventDefault on touchmove if content is not scrollable in either direction', () => { const el = document.createElement('div'); Object.defineProperty(el, 'clientHeight', { value: 200, configurable: true }); Object.defineProperty(el, 'scrollHeight', { value: 100, configurable: true }); + Object.defineProperty(el, 'clientWidth', { value: 200, configurable: true }); + Object.defineProperty(el, 'scrollWidth', { value: 100, configurable: true }); setupScrollLockGuard(el); @@ -53,4 +69,21 @@ describe('scroll-lock', () => { expect(preventDefaultSpy).toHaveBeenCalled(); }); + + it('should NOT preventDefault on touchmove if content is horizontally scrollable', () => { + const el = document.createElement('div'); + Object.defineProperty(el, 'clientHeight', { value: 200, configurable: true }); + Object.defineProperty(el, 'scrollHeight', { value: 100, configurable: true }); + Object.defineProperty(el, 'clientWidth', { value: 100, configurable: true }); + Object.defineProperty(el, 'scrollWidth', { value: 300, configurable: true }); + + setupScrollLockGuard(el); + + const touchMoveEvent = new Event('touchmove', { bubbles: true, cancelable: true }); + const preventDefaultSpy = vi.spyOn(touchMoveEvent, 'preventDefault'); + + el.dispatchEvent(touchMoveEvent); + + expect(preventDefaultSpy).not.toHaveBeenCalled(); + }); }); diff --git a/packages/mobile-devtools/src/ui/utils/json-highlighter.ts b/packages/mobile-devtools/src/ui/utils/json-highlighter.ts index 8b3ba39..c9a8f7d 100644 --- a/packages/mobile-devtools/src/ui/utils/json-highlighter.ts +++ b/packages/mobile-devtools/src/ui/utils/json-highlighter.ts @@ -1,3 +1,9 @@ +/** + * Converts JSON objects or raw JSON strings into syntax-highlighted HTML string representation. + * Uses CSS CSS variables (`--json-key`, `--json-string`, `--json-number`, `--json-boolean`, `--json-null`). + * @param data JSON object or raw JSON string. + * @returns HTML string with inline syntax highlighted spans. + */ export function highlightJsonSyntax(data: any): string { if (data === null || data === undefined) return ''; diff --git a/packages/mobile-devtools/src/ui/utils/scroll-lock.ts b/packages/mobile-devtools/src/ui/utils/scroll-lock.ts index 5fc27a7..64418dc 100644 --- a/packages/mobile-devtools/src/ui/utils/scroll-lock.ts +++ b/packages/mobile-devtools/src/ui/utils/scroll-lock.ts @@ -1,20 +1,39 @@ +/** + * Prevents mobile browser overscroll bounce (rubber-banding) from bleeding through to host web page. + * Supports both vertical and horizontal scrollable containers without blocking touch gestures. + * @param el Target scrollable HTML element. + */ export function setupScrollLockGuard(el: HTMLElement) { el.style.overscrollBehavior = 'contain'; (el.style as any).webkitOverflowScrolling = 'touch'; - el.style.touchAction = 'pan-y'; + el.style.touchAction = 'pan-x pan-y'; el.addEventListener( 'touchstart', () => { + // 1. Vertical bounce guard const top = el.scrollTop; - const total = el.scrollHeight; - const current = top + el.clientHeight; + const totalH = el.scrollHeight; + const currentH = top + el.clientHeight; - if (total > el.clientHeight) { + if (totalH > el.clientHeight) { if (top <= 0) { el.scrollTop = 1; - } else if (current >= total) { - el.scrollTop = total - el.clientHeight - 1; + } else if (currentH >= totalH) { + el.scrollTop = totalH - el.clientHeight - 1; + } + } + + // 2. Horizontal bounce guard + const left = el.scrollLeft; + const totalW = el.scrollWidth; + const currentW = left + el.clientWidth; + + if (totalW > el.clientWidth) { + if (left <= 0) { + el.scrollLeft = 1; + } else if (currentW >= totalW) { + el.scrollLeft = totalW - el.clientWidth - 1; } } }, @@ -24,7 +43,11 @@ export function setupScrollLockGuard(el: HTMLElement) { el.addEventListener( 'touchmove', (e: TouchEvent) => { - if (el.scrollHeight <= el.clientHeight) { + const isVerticallyScrollable = el.scrollHeight > el.clientHeight; + const isHorizontallyScrollable = el.scrollWidth > el.clientWidth; + + // Only prevent default if element cannot scroll in EITHER direction + if (!isVerticallyScrollable && !isHorizontallyScrollable) { if (e.cancelable) { e.preventDefault(); } diff --git a/packages/mobile-devtools/src/ui/utils/theme-helper.ts b/packages/mobile-devtools/src/ui/utils/theme-helper.ts index 8ee4829..d7103e9 100644 --- a/packages/mobile-devtools/src/ui/utils/theme-helper.ts +++ b/packages/mobile-devtools/src/ui/utils/theme-helper.ts @@ -1,5 +1,11 @@ import { DevToolsTheme, ThemeMode } from '../../core'; +/** + * Calculates perceived color brightness using the YIQ formula to determine if a HEX color is light or dark. + * Used to automatically contrast UI text and card colors when custom background colors are applied. + * @param hex HEX color string (e.g. '#ffffff', '#121212', '#fff'). + * @returns True if perceived brightness >= 128 (light color), false otherwise. + */ function isLightColor(hex: string): boolean { if (!hex || typeof hex !== 'string') return false; const color = hex.replace('#', ''); @@ -25,6 +31,13 @@ function isLightColor(hex: string): boolean { return yiq >= 128; } +/** + * Applies custom theme CSS variable overrides to the DevTools Shadow DOM container element. + * Automatically calculates contrast text/card colors if custom background color is provided. + * @param container Target DevTools Shadow DOM container element. + * @param theme Custom theme configuration object. + * @param effectiveMode Effective calculated theme mode ('dark' or 'light'). + */ export function applyThemeVariables( container: HTMLElement, theme?: DevToolsTheme, @@ -129,6 +142,10 @@ export function applyThemeVariables( } } +/** + * Internal helper to remove custom theme CSS custom properties from Shadow DOM container. + * @param container Target DevTools Shadow DOM container element. + */ function clearAllThemeOverrides(container: HTMLElement) { container.style.removeProperty('--dev-bg'); container.style.removeProperty('--dev-bg-200'); diff --git a/packages/mobile-devtools/src/ui/views/__tests__/floating-badge.test.ts b/packages/mobile-devtools/src/ui/views/__tests__/floating-badge.test.ts index bf525dd..2335a96 100644 --- a/packages/mobile-devtools/src/ui/views/__tests__/floating-badge.test.ts +++ b/packages/mobile-devtools/src/ui/views/__tests__/floating-badge.test.ts @@ -43,4 +43,10 @@ describe('FloatingBadgeView', () => { const el = badgeView.render(); expect(el.style.opacity).toBe('0.8'); }); + + it('should hide badge element when showBadge is false', () => { + store.updateConfig({ showBadge: false }); + const el = badgeView.render(); + expect(el.style.display).toBe('none'); + }); }); diff --git a/packages/mobile-devtools/src/ui/views/drawer.ts b/packages/mobile-devtools/src/ui/views/drawer.ts index 7937339..0e5511d 100644 --- a/packages/mobile-devtools/src/ui/views/drawer.ts +++ b/packages/mobile-devtools/src/ui/views/drawer.ts @@ -4,6 +4,7 @@ import { DevToolsStore, DevToolsTabId, exportBugReport, + formatCount, THEME_MODES, } from '../../core'; import { CLOSE_ICON, LOGO_ICON, MOON_ICON, SHARE_ICON, SUN_ICON } from '../icons'; @@ -31,10 +32,11 @@ export class DrawerView { private storageTab: StorageTabView; private systemTab: SystemTabView; - // Swipe Dismiss State + // Swipe Dismiss & Tab Scroll State private dragOffsetY = 0; private isSwiping = false; private swipeStartY: number | null = null; + private tabsScrollLeft = 0; constructor(store: DevToolsStore) { this.store = store; @@ -244,7 +246,7 @@ export class DrawerView { const pillTitle = config.title || 'Mobile DevTools'; const errorPill = unread.errors > 0 - ? `${unread.errors} Errors` + ? `${formatCount(unread.errors)} Errors` : ''; titleGroup.innerHTML = ` @@ -300,10 +302,15 @@ export class DrawerView { this.attachSwipeListeners(header); // Segmented Tabs Bar + const prevTabsScrollLeft = this.tabsScrollLeft; const tabsBar = document.createElement('div'); tabsBar.className = 'devtools-tabs-bar'; setupScrollLockGuard(tabsBar); + tabsBar.addEventListener('scroll', () => { + this.tabsScrollLeft = tabsBar.scrollLeft; + }); + const enabledTabs = config.enabledTabs || [ BUILTIN_TABS.CONSOLE, BUILTIN_TABS.ELEMENTS, @@ -359,6 +366,12 @@ export class DrawerView { this.drawerElement.appendChild(header); this.drawerElement.appendChild(tabsBar); this.drawerElement.appendChild(this.tabContentContainer); + + requestAnimationFrame(() => { + if (tabsBar) { + tabsBar.scrollLeft = prevTabsScrollLeft; + } + }); } public destroy() { diff --git a/packages/mobile-devtools/src/ui/views/floating-badge.ts b/packages/mobile-devtools/src/ui/views/floating-badge.ts index e849b45..bd6f984 100644 --- a/packages/mobile-devtools/src/ui/views/floating-badge.ts +++ b/packages/mobile-devtools/src/ui/views/floating-badge.ts @@ -3,6 +3,7 @@ import { clampPositionToViewport, DevToolsStore, DragPosition, + formatCount, } from '../../core'; export class FloatingBadgeView { @@ -45,6 +46,13 @@ export class FloatingBadgeView { const unread = this.store.getUnreadCounts(); const config = this.store.getConfig(); + if (config.showBadge === false) { + this.badgeElement.style.display = 'none'; + return; + } else { + this.badgeElement.style.display = 'flex'; + } + const hasErrors = unread.errors > 0; const hasWarnings = unread.warnings > 0; const hasUnreadTotal = unread.total > 0; @@ -61,8 +69,8 @@ export class FloatingBadgeView { } this.badgeElement.style.borderColor = borderColor; - // Custom badge styling from config.styles?.badge or legacy config.badgeStyle - const customBadgeStyles = config.styles?.badge || config.badgeStyle; + // Custom badge styling from config.styles?.badge + const customBadgeStyles = config.styles?.badge; if (customBadgeStyles) { Object.assign(this.badgeElement.style, customBadgeStyles); } @@ -89,11 +97,11 @@ export class FloatingBadgeView { let tagHtml = ''; if (hasErrors) { - tagHtml = `${unread.errors > 99 ? '99+' : unread.errors}`; + tagHtml = `${formatCount(unread.errors)}`; } else if (hasWarnings) { - tagHtml = `${unread.warnings}`; + tagHtml = `${formatCount(unread.warnings)}`; } else if (hasUnreadTotal) { - tagHtml = `${unread.total}`; + tagHtml = `${formatCount(unread.total)}`; } this.badgeElement.innerHTML = ` diff --git a/packages/mobile-devtools/src/ui/views/tabs/console-tab.ts b/packages/mobile-devtools/src/ui/views/tabs/console-tab.ts index a3af220..84d8a73 100644 --- a/packages/mobile-devtools/src/ui/views/tabs/console-tab.ts +++ b/packages/mobile-devtools/src/ui/views/tabs/console-tab.ts @@ -1,7 +1,6 @@ import { DevToolsStore, formatTimestamp, LOG_LEVELS } from '../../../core'; import { renderJsonTree } from '../../components/json-tree'; import { TRASH_ICON } from '../../icons'; - import { setupScrollLockGuard } from '../../utils/scroll-lock'; export class ConsoleTabView { @@ -11,6 +10,8 @@ export class ConsoleTabView { private clearBtn: HTMLButtonElement | null = null; private searchValue = ''; private levelFilter: string = 'all'; + private savedScrollTop = 0; + private isScrolledToBottom = true; constructor(store: DevToolsStore) { this.store = store; @@ -73,6 +74,16 @@ export class ConsoleTabView { this.listScrollContainer = document.createElement('div'); this.listScrollContainer.className = 'devtools-list-scroll'; setupScrollLockGuard(this.listScrollContainer); + this.listScrollContainer.addEventListener('scroll', () => { + if (!this.listScrollContainer) return; + const isAtBottom = + this.listScrollContainer.scrollHeight - + this.listScrollContainer.scrollTop - + this.listScrollContainer.clientHeight < + 40; + this.isScrolledToBottom = isAtBottom; + this.savedScrollTop = this.listScrollContainer.scrollTop; + }); this.container.appendChild(toolbar); this.container.appendChild(this.listScrollContainer); @@ -83,6 +94,8 @@ export class ConsoleTabView { public updateList() { if (!this.listScrollContainer) return; + const prevScrollTop = this.savedScrollTop; + const shouldScrollToBottom = this.isScrolledToBottom; this.listScrollContainer.innerHTML = ''; const logs = this.store.getLogs(); @@ -159,5 +172,15 @@ export class ConsoleTabView { card.appendChild(content); this.listScrollContainer!.appendChild(card); }); + + requestAnimationFrame(() => { + if (this.listScrollContainer) { + if (shouldScrollToBottom) { + this.listScrollContainer.scrollTop = this.listScrollContainer.scrollHeight; + } else { + this.listScrollContainer.scrollTop = prevScrollTop; + } + } + }); } } diff --git a/packages/mobile-devtools/src/ui/views/tabs/elements-tab.ts b/packages/mobile-devtools/src/ui/views/tabs/elements-tab.ts index 6c90820..582c3a8 100644 --- a/packages/mobile-devtools/src/ui/views/tabs/elements-tab.ts +++ b/packages/mobile-devtools/src/ui/views/tabs/elements-tab.ts @@ -1,4 +1,4 @@ -import { DevToolsStore, ElementsManager, isBrowser, STYLE_CATEGORIES } from '../../../core'; +import { DevToolsStore, ElementsManager, isBrowser } from '../../../core'; import { CHEVRON_DOWN_ICON, CHEVRON_RIGHT_ICON, diff --git a/packages/mobile-devtools/src/ui/views/tabs/network-tab.ts b/packages/mobile-devtools/src/ui/views/tabs/network-tab.ts index de35eec..1f6b26e 100644 --- a/packages/mobile-devtools/src/ui/views/tabs/network-tab.ts +++ b/packages/mobile-devtools/src/ui/views/tabs/network-tab.ts @@ -1,6 +1,7 @@ import { copyToClipboard, DevToolsStore, + formatCount, formatDuration, formatTimestamp, generateCurlCommand, @@ -22,9 +23,14 @@ export class NetworkTabView { private searchValue = ''; private methodFilter = 'ALL'; private selectedReq: NetworkRequestEntry | null = null; - private activeDetailTab: 'response' | 'payload' | 'headers' = 'response'; + private activeDetailTab: 'response' | 'payload' | 'headers' | 'frames' = 'response'; private responseViewMode: 'parsed' | 'raw' = 'parsed'; private payloadViewMode: 'parsed' | 'raw' = 'parsed'; + private detailScrollTop = 0; + private detailScrollLeft = 0; + private isDetailScrolledToBottom = true; + private mainListScrollTop = 0; + private subTabsScrollLeft = 0; constructor(store: DevToolsStore) { this.store = store; @@ -36,7 +42,11 @@ export class NetworkTabView { this.container.innerHTML = ''; if (this.selectedReq) { - return this.renderDetailModal(this.selectedReq); + const freshReq = + this.store.getNetworkRequests().find((r) => r.id === this.selectedReq!.id) || + this.selectedReq; + this.selectedReq = freshReq; + return this.renderDetailModal(freshReq); } // Toolbar @@ -53,6 +63,8 @@ export class NetworkTabView { + + `; methodSelect.value = this.methodFilter; methodSelect.addEventListener('change', (e) => { @@ -112,6 +124,11 @@ export class NetworkTabView { this.listScrollContainer = document.createElement('div'); this.listScrollContainer.className = 'devtools-list-scroll'; setupScrollLockGuard(this.listScrollContainer); + this.listScrollContainer.addEventListener('scroll', () => { + if (this.listScrollContainer) { + this.mainListScrollTop = this.listScrollContainer.scrollTop; + } + }); this.container.appendChild(toolbar); this.container.appendChild(this.listScrollContainer); @@ -122,6 +139,7 @@ export class NetworkTabView { public updateList() { if (!this.listScrollContainer) return; + const prevScrollTop = this.mainListScrollTop; this.listScrollContainer.innerHTML = ''; const requests = this.store.getNetworkRequests(); @@ -161,6 +179,8 @@ export class NetworkTabView { row.addEventListener('click', () => { this.selectedReq = req; this.activeDetailTab = 'response'; + this.detailScrollTop = 0; + this.isDetailScrolledToBottom = true; this.render(); }); @@ -221,6 +241,12 @@ export class NetworkTabView { row.appendChild(rightGroup); this.listScrollContainer!.appendChild(row); }); + + requestAnimationFrame(() => { + if (this.listScrollContainer) { + this.listScrollContainer.scrollTop = prevScrollTop; + } + }); } private renderDetailModal(req: NetworkRequestEntry): HTMLElement { @@ -330,12 +356,18 @@ export class NetworkTabView { `; // Tabs Bar (Response / Payload / Headers) + Parsed/Raw inline + const prevSubTabsScrollLeft = this.subTabsScrollLeft; const tabsBar = document.createElement('div'); tabsBar.className = 'devtools-tabs-bar'; tabsBar.style.display = 'flex'; tabsBar.style.alignItems = 'center'; tabsBar.style.justifyContent = 'flex-start'; tabsBar.style.gap = '6px'; + setupScrollLockGuard(tabsBar); + + tabsBar.addEventListener('scroll', () => { + this.subTabsScrollLeft = tabsBar.scrollLeft; + }); const respBtn = document.createElement('button'); respBtn.className = `devtools-tab-btn ${this.activeDetailTab === 'response' ? 'active' : ''}`; @@ -361,9 +393,18 @@ export class NetworkTabView { this.render(); }); + const framesBtn = document.createElement('button'); + framesBtn.className = `devtools-tab-btn ${this.activeDetailTab === 'frames' ? 'active' : ''}`; + framesBtn.textContent = `Frames (${formatCount(req.frames?.length || 0)})`; + framesBtn.addEventListener('click', () => { + this.activeDetailTab = 'frames'; + this.render(); + }); + tabsBar.appendChild(respBtn); tabsBar.appendChild(payloadBtn); tabsBar.appendChild(headersBtn); + tabsBar.appendChild(framesBtn); // Left-aligned Parsed / Raw segmented control & Copy Request button inline if (this.activeDetailTab === 'response' || this.activeDetailTab === 'payload') { @@ -411,6 +452,13 @@ export class NetworkTabView { bodyScroll.style.padding = '14px'; setupScrollLockGuard(bodyScroll); + bodyScroll.addEventListener('scroll', () => { + const isAtBottom = + bodyScroll.scrollHeight - bodyScroll.scrollTop - bodyScroll.clientHeight < 40; + this.isDetailScrolledToBottom = isAtBottom; + this.detailScrollTop = bodyScroll.scrollTop; + }); + if (this.activeDetailTab === 'response') { if (!req.responseBody) { bodyScroll.innerHTML = @@ -492,6 +540,72 @@ export class NetworkTabView { headersWrap.appendChild(resH4); headersWrap.appendChild(resTable); bodyScroll.appendChild(headersWrap); + } else if (this.activeDetailTab === 'frames') { + const frames = req.frames || []; + if (frames.length === 0) { + bodyScroll.innerHTML = + '
No WebSocket/SSE frame messages recorded.
'; + } else { + const frameWrap = document.createElement('div'); + frameWrap.style.display = 'flex'; + frameWrap.style.flexDirection = 'column'; + frameWrap.style.gap = '8px'; + + frames.forEach((frame) => { + const item = document.createElement('div'); + item.style.padding = '8px 10px'; + item.style.borderRadius = '6px'; + item.style.background = 'var(--dev-card-bg)'; + item.style.border = '1px solid var(--dev-border)'; + item.style.fontSize = '12px'; + item.style.fontFamily = 'var(--dev-font-mono)'; + + const header = document.createElement('div'); + header.style.display = 'flex'; + header.style.justifyContent = 'space-between'; + header.style.alignItems = 'center'; + header.style.marginBottom = '4px'; + + const badge = document.createElement('span'); + badge.style.padding = '2px 6px'; + badge.style.borderRadius = '4px'; + badge.style.fontSize = '10px'; + badge.style.fontWeight = '700'; + if (frame.type === 'sent') { + badge.style.background = 'rgba(16, 185, 129, 0.15)'; + badge.style.color = 'var(--dev-success)'; + badge.textContent = 'โฌ† Sent'; + } else { + badge.style.background = 'rgba(59, 130, 246, 0.15)'; + badge.style.color = 'var(--dev-accent)'; + badge.textContent = 'โฌ‡ Received'; + } + + const time = document.createElement('span'); + time.style.color = 'var(--dev-text-muted)'; + time.style.fontSize = '11px'; + time.textContent = formatTimestamp(frame.timestamp); + + header.appendChild(badge); + header.appendChild(time); + + const dataEl = document.createElement('div'); + dataEl.style.color = 'var(--dev-text-bright)'; + dataEl.style.wordBreak = 'break-all'; + dataEl.style.whiteSpace = 'pre-wrap'; + if (typeof frame.data === 'object' && frame.data !== null) { + dataEl.appendChild(renderJsonTree(frame.data)); + } else { + dataEl.textContent = String(frame.data); + } + + item.appendChild(header); + item.appendChild(dataEl); + frameWrap.appendChild(item); + }); + + bodyScroll.appendChild(frameWrap); + } } bodyContainer.appendChild(bodyScroll); @@ -502,6 +616,21 @@ export class NetworkTabView { modal.appendChild(bodyContainer); this.container.appendChild(modal); + + requestAnimationFrame(() => { + if (tabsBar) { + tabsBar.scrollLeft = prevSubTabsScrollLeft; + } + if (bodyScroll) { + bodyScroll.scrollLeft = this.detailScrollLeft; + } + if (this.isDetailScrolledToBottom && this.activeDetailTab === 'frames') { + bodyScroll.scrollTop = bodyScroll.scrollHeight; + } else { + bodyScroll.scrollTop = this.detailScrollTop; + } + }); + return this.container; } } diff --git a/packages/mobile-devtools/tsconfig.json b/packages/mobile-devtools/tsconfig.json index 4c7db85..9ce53de 100644 --- a/packages/mobile-devtools/tsconfig.json +++ b/packages/mobile-devtools/tsconfig.json @@ -10,6 +10,8 @@ "declarationMap": true, "sourceMap": true, "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, "skipLibCheck": true }, "include": ["src/**/*"], diff --git a/packages/mobile-devtools/vite.config.ts b/packages/mobile-devtools/vite.config.ts index ce59ace..cadbfe0 100644 --- a/packages/mobile-devtools/vite.config.ts +++ b/packages/mobile-devtools/vite.config.ts @@ -19,6 +19,7 @@ export default defineConfig({ index: resolve(__dirname, 'src/index.ts'), react: resolve(__dirname, 'src/react.ts'), vue: resolve(__dirname, 'src/vue.ts'), + svelte: resolve(__dirname, 'src/svelte.ts'), }, formats: ['es'], }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d3efdf5..5981397 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,6 +9,9 @@ catalogs: '@playwright/test': specifier: ^1.50.1 version: 1.62.1 + '@sveltejs/vite-plugin-svelte': + specifier: ^5.0.0 + version: 5.1.1 '@types/node': specifier: ^22.15.3 version: 22.15.3 @@ -39,6 +42,9 @@ catalogs: react-dom: specifier: ^19.2.0 version: 19.2.0 + svelte: + specifier: ^5.0.0 + version: 5.56.9 turbo: specifier: ^2.10.10 version: 2.10.10 @@ -135,6 +141,12 @@ importers: eslint: specifier: ^9.39.1 version: 9.39.1(jiti@2.7.0) + eslint-import-resolver-typescript: + specifier: ^4.4.5 + version: 4.4.5(eslint@9.39.1(jiti@2.7.0)) + eslint-plugin-boundaries: + specifier: ^7.2.0 + version: 7.2.0(eslint-import-resolver-typescript@4.4.5(eslint@9.39.1(jiti@2.7.0)))(eslint@9.39.1(jiti@2.7.0)) postcss: specifier: ^8.5.26 version: 8.5.26 @@ -188,6 +200,31 @@ importers: specifier: 'catalog:' version: 6.4.3(@types/node@22.15.3)(jiti@2.7.0)(lightningcss@1.32.0) + examples/svelte: + dependencies: + mobile-devtools: + specifier: workspace:* + version: link:../../packages/mobile-devtools + svelte: + specifier: 'catalog:' + version: 5.56.9(@typescript-eslint/types@8.50.0) + devDependencies: + '@mobile-devtools/tsconfig': + specifier: workspace:* + version: link:../../packages/config/typescript + '@mobile-devtools/vite-config': + specifier: workspace:* + version: link:../../packages/config/vite + '@sveltejs/vite-plugin-svelte': + specifier: 'catalog:' + version: 5.1.1(svelte@5.56.9(@typescript-eslint/types@8.50.0))(vite@6.4.3(@types/node@22.15.3)(jiti@2.7.0)(lightningcss@1.32.0)) + typescript: + specifier: 'catalog:' + version: 5.9.2 + vite: + specifier: 'catalog:' + version: 6.4.3(@types/node@22.15.3)(jiti@2.7.0)(lightningcss@1.32.0) + examples/vanilla: dependencies: mobile-devtools: @@ -301,6 +338,9 @@ importers: react-dom: specifier: 'catalog:' version: 19.2.0(react@19.2.0) + svelte: + specifier: 'catalog:' + version: 5.56.9(@typescript-eslint/types@8.50.0) typescript: specifier: 'catalog:' version: 5.9.2 @@ -417,6 +457,10 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} + '@boundaries/elements@3.1.1': + resolution: {integrity: sha512-ikqee8669yp2I+uLG67T5P5BSbe+y4NqW5Mk+7tYa+Ydzhpmh0pDyMaH9918J2Ll36ZqYKDb4g4QBeMpWg7VDg==} + engines: {node: '>=18.18'} + '@cloudflare/kv-asset-handler@0.5.0': resolution: {integrity: sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==} engines: {node: '>=22.0.0'} @@ -529,9 +573,18 @@ packages: resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + '@emnapi/runtime@1.11.3': resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@esbuild/aix-ppc64@0.25.12': resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} engines: {node: '>=18'} @@ -1083,6 +1136,13 @@ packages: cpu: [x64] os: [linux] + '@napi-rs/wasm-runtime@1.2.3': + resolution: {integrity: sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} + peerDependencies: + '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4 + '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4 + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -1245,6 +1305,26 @@ packages: '@speed-highlight/core@1.2.24': resolution: {integrity: sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==} + '@sveltejs/acorn-typescript@1.0.13': + resolution: {integrity: sha512-wgKggnhZVL9Bfx1OaKKTrYY9BFRk6C8UAkQNUcIv1+llzYrIqy+RZm5HPKzn0NpEBvTVhTqB4kQyllZywsRBRQ==} + peerDependencies: + acorn: ^8.9.0 + + '@sveltejs/vite-plugin-svelte-inspector@4.0.1': + resolution: {integrity: sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22} + peerDependencies: + '@sveltejs/vite-plugin-svelte': ^5.0.0 + svelte: ^5.0.0 + vite: ^6.0.0 + + '@sveltejs/vite-plugin-svelte@5.1.1': + resolution: {integrity: sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22} + peerDependencies: + svelte: ^5.0.0 + vite: ^6.0.0 + '@tailwindcss/node@4.3.3': resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==} @@ -1363,6 +1443,9 @@ packages: cpu: [arm64] os: [win32] + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -1401,6 +1484,9 @@ packages: '@types/react@19.2.2': resolution: {integrity: sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@typescript-eslint/eslint-plugin@8.50.0': resolution: {integrity: sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1460,6 +1546,116 @@ packages: resolution: {integrity: sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@unrs/resolver-binding-android-arm-eabi@1.12.2': + resolution: {integrity: sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==} + cpu: [arm] + os: [android] + + '@unrs/resolver-binding-android-arm64@1.12.2': + resolution: {integrity: sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==} + cpu: [arm64] + os: [android] + + '@unrs/resolver-binding-darwin-arm64@1.12.2': + resolution: {integrity: sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==} + cpu: [arm64] + os: [darwin] + + '@unrs/resolver-binding-darwin-x64@1.12.2': + resolution: {integrity: sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==} + cpu: [x64] + os: [darwin] + + '@unrs/resolver-binding-freebsd-x64@1.12.2': + resolution: {integrity: sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==} + cpu: [x64] + os: [freebsd] + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2': + resolution: {integrity: sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2': + resolution: {integrity: sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-gnu@1.12.2': + resolution: {integrity: sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-musl@1.12.2': + resolution: {integrity: sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': + resolution: {integrity: sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==} + cpu: [loong64] + os: [linux] + + '@unrs/resolver-binding-linux-loong64-musl@1.12.2': + resolution: {integrity: sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==} + cpu: [loong64] + os: [linux] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': + resolution: {integrity: sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==} + cpu: [ppc64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': + resolution: {integrity: sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': + resolution: {integrity: sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': + resolution: {integrity: sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==} + cpu: [s390x] + os: [linux] + + '@unrs/resolver-binding-linux-x64-gnu@1.12.2': + resolution: {integrity: sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-linux-x64-musl@1.12.2': + resolution: {integrity: sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-openharmony-arm64@1.12.2': + resolution: {integrity: sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==} + cpu: [arm64] + os: [openharmony] + + '@unrs/resolver-binding-wasm32-wasi@1.12.2': + resolution: {integrity: sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': + resolution: {integrity: sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==} + cpu: [arm64] + os: [win32] + + '@unrs/resolver-binding-win32-ia32-msvc@1.12.2': + resolution: {integrity: sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==} + cpu: [ia32] + os: [win32] + + '@unrs/resolver-binding-win32-x64-msvc@1.12.2': + resolution: {integrity: sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==} + cpu: [x64] + os: [win32] + '@vitejs/plugin-react@4.7.0': resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} engines: {node: ^14.18.0 || >=16.0.0} @@ -1611,6 +1807,10 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + aria-query@5.3.1: + resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==} + engines: {node: '>= 0.4'} + array-buffer-byte-length@1.0.2: resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} engines: {node: '>= 0.4'} @@ -1654,6 +1854,10 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -1679,6 +1883,10 @@ packages: resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} engines: {node: 20 || >=22} + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + browserslist@4.28.8: resolution: {integrity: sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -1782,6 +1990,14 @@ packages: de-indent@1.0.2: resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -1801,6 +2017,10 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} @@ -1813,6 +2033,9 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + devalue@5.9.0: + resolution: {integrity: sha512-RWrqdArjvPbsATEhOPUo6Wndc/iWnkWKlhIrdlF3zMMYo/c3CVtoaVAyLtWxz5h8nSlkHzxnzV2uLydPXmtF+A==} + doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} @@ -1911,6 +2134,58 @@ packages: peerDependencies: eslint: '>=7.0.0' + eslint-import-context@0.1.9: + resolution: {integrity: sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + peerDependencies: + unrs-resolver: ^1.0.0 + peerDependenciesMeta: + unrs-resolver: + optional: true + + eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + + eslint-import-resolver-typescript@4.4.5: + resolution: {integrity: sha512-nbE5XLph6TLtGYcu/U6e6ZVXyKBhbDWK5cLGk76eJ7NdZpwf1P9EFkpt1Z01mNZNrrilsAYWKH6zUkL4reoXbw==} + engines: {node: ^16.17.0 || >=18.6.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + eslint-plugin-import-x: '*' + peerDependenciesMeta: + eslint-plugin-import: + optional: true + eslint-plugin-import-x: + optional: true + + eslint-module-utils@2.12.1: + resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-boundaries@7.2.0: + resolution: {integrity: sha512-ywSaVhn9dBFPxEoUTXnu1106Er+NhSSxTuiz52H+SUgAhd0kmikHHhdavv2d+6Aw7f7w1RDT2cXQUJ/7D/V68A==} + engines: {node: '>=18.18'} + peerDependencies: + eslint: '>=6.0.0' + eslint-plugin-only-warn@1.1.0: resolution: {integrity: sha512-2tktqUAT+Q3hCAU0iSf4xAN1k9zOpjK5WO8104mB0rT/dGhOa09582HN5HlbxNbPRZ0THV7nLGvzugcNOSjzfA==} engines: {node: '>=6'} @@ -1955,6 +2230,9 @@ packages: jiti: optional: true + esm-env@1.2.2: + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} + espree@10.4.0: resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1963,6 +2241,14 @@ packages: resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} engines: {node: '>=0.10'} + esrap@2.3.4: + resolution: {integrity: sha512-CHnJXZDfBeOAjLIGNy/0BEsmDu5+irHgUXRW9pGptxCbO3uyjJ98crilzFgiBqizW94UodjX3GAPLg/dhEnUfA==} + peerDependencies: + '@typescript-eslint/types': ^8.2.0 + peerDependenciesMeta: + '@typescript-eslint/types': + optional: true + esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} engines: {node: '>=4.0'} @@ -2010,6 +2296,10 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -2065,6 +2355,9 @@ packages: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} + get-tsconfig@4.14.2: + resolution: {integrity: sha512-XpwZALwwl/BaKTAyC6+c5T8y6kCg2jk+XGqOVrKIQmW49pNypYLMRjCUXqa28tQgJlhS2RlzP7sc+Rx7W6qsfw==} + glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} @@ -2097,6 +2390,11 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + handlebars@4.7.9: + resolution: {integrity: sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==} + engines: {node: '>=0.4.7'} + hasBin: true + has-bigints@1.1.0: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} @@ -2183,6 +2481,9 @@ packages: resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} engines: {node: '>= 0.4'} + is-bun-module@2.0.0: + resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} + is-callable@1.2.7: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} @@ -2231,9 +2532,16 @@ packages: resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} engines: {node: '>= 0.4'} + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-reference@3.0.3: + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -2436,6 +2744,9 @@ packages: resolution: {integrity: sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==} engines: {node: '>=14'} + locate-character@3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -2479,6 +2790,10 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + miniflare@5.20260801.0-alpha: resolution: {integrity: sha512-AfMrnQbJg81ESsGkGhOkHBtwTqCG+mosR+3GE+qDrhF1I/ieIvgg3ICxNyBvgHBZ3iapy6cysBQHNPykkzrfgw==} engines: {node: '>=22.0.0'} @@ -2498,6 +2813,9 @@ packages: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minipass@7.1.3: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} @@ -2516,9 +2834,17 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + napi-postinstall@0.3.4: + resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + node-releases@2.0.53: resolution: {integrity: sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==} engines: {node: '>=18'} @@ -2615,6 +2941,10 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + picomatch@4.0.3: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} @@ -2707,6 +3037,14 @@ packages: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + resolve@2.0.0-next.5: resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} hasBin: true @@ -2809,6 +3147,14 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + stable-hash-x@0.2.0: + resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==} + engines: {node: '>=12.0.0'} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -2873,6 +3219,10 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + svelte@5.56.9: + resolution: {integrity: sha512-VT8kSnlEg8069w7AiCcAk3Yf5xvMnrGTagVOmU/OpOLHaHnNqXhWZCH/4EVga/bT/HtWhvE6/fHrXLErx7OnJA==} + engines: {node: '>=18'} + symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} @@ -2919,6 +3269,10 @@ packages: resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} hasBin: true + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + tough-cookie@5.1.2: resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} engines: {node: '>=16'} @@ -2975,6 +3329,11 @@ packages: ufo@1.6.4: resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} + uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} + hasBin: true + unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -3027,6 +3386,9 @@ packages: resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} engines: {node: '>=18.12.0'} + unrs-resolver@1.12.2: + resolution: {integrity: sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==} + update-browserslist-db@1.3.1: resolution: {integrity: sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==} hasBin: true @@ -3095,6 +3457,14 @@ packages: yaml: optional: true + vitefu@1.1.3: + resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + vite: + optional: true + vitest@3.2.7: resolution: {integrity: sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -3194,6 +3564,9 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + workerd@1.20260801.1: resolution: {integrity: sha512-/g9JGTyqnHtoIscpBHqKD8swE2V4StBs2i69PmLiOhH45OP95jCFICl4F1hKlgN57rqfni5LiCitttoX5OOkVA==} engines: {node: '>=16'} @@ -3266,6 +3639,9 @@ packages: youch@4.1.0-beta.10: resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} + zimmerframe@1.1.4: + resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} + snapshots: '@alloc/quick-lru@5.2.0': {} @@ -3397,6 +3773,20 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} + '@boundaries/elements@3.1.1(eslint-import-resolver-typescript@4.4.5(eslint@9.39.1(jiti@2.7.0)))(eslint@9.39.1(jiti@2.7.0))': + dependencies: + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.5(eslint@9.39.1(jiti@2.7.0)))(eslint@9.39.1(jiti@2.7.0)) + handlebars: 4.7.9 + is-core-module: 2.16.1 + micromatch: 4.0.8 + transitivePeerDependencies: + - '@typescript-eslint/parser' + - eslint + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + '@cloudflare/kv-asset-handler@0.5.0': {} '@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260801.1)': @@ -3478,11 +3868,27 @@ snapshots: '@csstools/css-tokenizer@3.0.4': {} + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/runtime@1.11.3': dependencies: tslib: 2.8.1 optional: true + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild/aix-ppc64@0.25.12': optional: true @@ -3845,6 +4251,13 @@ snapshots: '@napi-rs/lzma-linux-x64-gnu@1.5.1': optional: true + '@napi-rs/wasm-runtime@1.2.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.3 + optional: true + '@pkgjs/parseargs@0.11.0': optional: true @@ -3953,6 +4366,32 @@ snapshots: '@speed-highlight/core@1.2.24': {} + '@sveltejs/acorn-typescript@1.0.13(acorn@8.18.0)': + dependencies: + acorn: 8.18.0 + + '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.56.9(@typescript-eslint/types@8.50.0))(vite@6.4.3(@types/node@22.15.3)(jiti@2.7.0)(lightningcss@1.32.0)))(svelte@5.56.9(@typescript-eslint/types@8.50.0))(vite@6.4.3(@types/node@22.15.3)(jiti@2.7.0)(lightningcss@1.32.0))': + dependencies: + '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.56.9(@typescript-eslint/types@8.50.0))(vite@6.4.3(@types/node@22.15.3)(jiti@2.7.0)(lightningcss@1.32.0)) + debug: 4.4.3 + svelte: 5.56.9(@typescript-eslint/types@8.50.0) + vite: 6.4.3(@types/node@22.15.3)(jiti@2.7.0)(lightningcss@1.32.0) + transitivePeerDependencies: + - supports-color + + '@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.56.9(@typescript-eslint/types@8.50.0))(vite@6.4.3(@types/node@22.15.3)(jiti@2.7.0)(lightningcss@1.32.0))': + dependencies: + '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.56.9(@typescript-eslint/types@8.50.0))(vite@6.4.3(@types/node@22.15.3)(jiti@2.7.0)(lightningcss@1.32.0)))(svelte@5.56.9(@typescript-eslint/types@8.50.0))(vite@6.4.3(@types/node@22.15.3)(jiti@2.7.0)(lightningcss@1.32.0)) + debug: 4.4.3 + deepmerge: 4.3.1 + kleur: 4.1.5 + magic-string: 0.30.21 + svelte: 5.56.9(@typescript-eslint/types@8.50.0) + vite: 6.4.3(@types/node@22.15.3)(jiti@2.7.0)(lightningcss@1.32.0) + vitefu: 1.1.3(vite@6.4.3(@types/node@22.15.3)(jiti@2.7.0)(lightningcss@1.32.0)) + transitivePeerDependencies: + - supports-color + '@tailwindcss/node@4.3.3': dependencies: '@jridgewell/remapping': 2.3.5 @@ -4040,6 +4479,11 @@ snapshots: '@turbo/windows-arm64@2.10.10': optional: true + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.29.8 @@ -4086,6 +4530,8 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/trusted-types@2.0.7': {} + '@typescript-eslint/eslint-plugin@8.50.0(@typescript-eslint/parser@8.50.0(eslint@9.39.1(jiti@2.7.0))(typescript@5.9.2))(eslint@9.39.1(jiti@2.7.0))(typescript@5.9.2)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -4177,6 +4623,76 @@ snapshots: '@typescript-eslint/types': 8.50.0 eslint-visitor-keys: 4.2.1 + '@unrs/resolver-binding-android-arm-eabi@1.12.2': + optional: true + + '@unrs/resolver-binding-android-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-darwin-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.12.2': + optional: true + + '@unrs/resolver-binding-freebsd-x64@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-loong64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-openharmony-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.12.2': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.2.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.12.2': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.12.2': + optional: true + '@vitejs/plugin-react@4.7.0(vite@6.4.3(@types/node@22.15.3)(jiti@2.7.0)(lightningcss@1.32.0))': dependencies: '@babel/core': 7.29.7 @@ -4382,6 +4898,8 @@ snapshots: argparse@2.0.1: {} + aria-query@5.3.1: {} + array-buffer-byte-length@1.0.2: dependencies: call-bound: 1.0.4 @@ -4453,6 +4971,8 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 + axobject-query@4.1.0: {} + balanced-match@1.0.2: {} balanced-match@4.0.4: {} @@ -4474,6 +4994,10 @@ snapshots: dependencies: balanced-match: 4.0.4 + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + browserslist@4.28.8: dependencies: baseline-browser-mapping: 2.11.14 @@ -4582,6 +5106,10 @@ snapshots: de-indent@1.0.2: {} + debug@3.2.7: + dependencies: + ms: 2.1.3 + debug@4.4.3: dependencies: ms: 2.1.3 @@ -4592,6 +5120,8 @@ snapshots: deep-is@0.1.4: {} + deepmerge@4.3.1: {} + define-data-property@1.1.4: dependencies: es-define-property: 1.0.1 @@ -4606,6 +5136,8 @@ snapshots: detect-libc@2.1.2: {} + devalue@5.9.0: {} + doctrine@2.1.0: dependencies: esutils: 2.0.3 @@ -4806,6 +5338,59 @@ snapshots: dependencies: eslint: 9.39.1(jiti@2.7.0) + eslint-import-context@0.1.9(unrs-resolver@1.12.2): + dependencies: + get-tsconfig: 4.14.2 + stable-hash-x: 0.2.0 + optionalDependencies: + unrs-resolver: 1.12.2 + + eslint-import-resolver-node@0.3.9: + dependencies: + debug: 3.2.7 + is-core-module: 2.16.1 + resolve: 1.22.12 + transitivePeerDependencies: + - supports-color + + eslint-import-resolver-typescript@4.4.5(eslint@9.39.1(jiti@2.7.0)): + dependencies: + debug: 4.4.3 + eslint: 9.39.1(jiti@2.7.0) + eslint-import-context: 0.1.9(unrs-resolver@1.12.2) + get-tsconfig: 4.14.2 + is-bun-module: 2.0.0 + stable-hash-x: 0.2.0 + tinyglobby: 0.2.15 + unrs-resolver: 1.12.2 + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.5(eslint@9.39.1(jiti@2.7.0)))(eslint@9.39.1(jiti@2.7.0)): + dependencies: + debug: 3.2.7 + optionalDependencies: + eslint: 9.39.1(jiti@2.7.0) + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 4.4.5(eslint@9.39.1(jiti@2.7.0)) + transitivePeerDependencies: + - supports-color + + eslint-plugin-boundaries@7.2.0(eslint-import-resolver-typescript@4.4.5(eslint@9.39.1(jiti@2.7.0)))(eslint@9.39.1(jiti@2.7.0)): + dependencies: + '@boundaries/elements': 3.1.1(eslint-import-resolver-typescript@4.4.5(eslint@9.39.1(jiti@2.7.0)))(eslint@9.39.1(jiti@2.7.0)) + chalk: 4.1.2 + eslint: 9.39.1(jiti@2.7.0) + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.5(eslint@9.39.1(jiti@2.7.0)))(eslint@9.39.1(jiti@2.7.0)) + handlebars: 4.7.9 + micromatch: 4.0.8 + transitivePeerDependencies: + - '@typescript-eslint/parser' + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + eslint-plugin-only-warn@1.1.0: {} eslint-plugin-react-hooks@5.2.0(eslint@9.39.1(jiti@2.7.0)): @@ -4890,6 +5475,8 @@ snapshots: transitivePeerDependencies: - supports-color + esm-env@1.2.2: {} + espree@10.4.0: dependencies: acorn: 8.15.0 @@ -4900,6 +5487,12 @@ snapshots: dependencies: estraverse: 5.3.0 + esrap@2.3.4(@typescript-eslint/types@8.50.0): + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + optionalDependencies: + '@typescript-eslint/types': 8.50.0 + esrecurse@4.3.0: dependencies: estraverse: 5.3.0 @@ -4932,6 +5525,10 @@ snapshots: dependencies: flat-cache: 4.0.1 + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -4998,6 +5595,10 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.3.0 + get-tsconfig@4.14.2: + dependencies: + resolve-pkg-maps: 1.0.0 + glob-parent@6.0.2: dependencies: is-glob: 4.0.3 @@ -5030,6 +5631,15 @@ snapshots: graceful-fs@4.2.11: {} + handlebars@4.7.9: + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 + has-bigints@1.1.0: {} has-flag@4.0.0: {} @@ -5118,6 +5728,10 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 + is-bun-module@2.0.0: + dependencies: + semver: 7.8.5 + is-callable@1.2.7: {} is-core-module@2.16.1: @@ -5163,8 +5777,14 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 + is-number@7.0.0: {} + is-potential-custom-element-name@1.0.1: {} + is-reference@3.0.3: + dependencies: + '@types/estree': 1.0.9 + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -5368,6 +5988,8 @@ snapshots: pkg-types: 2.3.1 quansync: 0.2.11 + locate-character@3.0.0: {} + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -5408,6 +6030,11 @@ snapshots: math-intrinsics@1.1.0: {} + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + miniflare@5.20260801.0-alpha: dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -5444,6 +6071,8 @@ snapshots: dependencies: brace-expansion: 2.0.2 + minimist@1.2.8: {} + minipass@7.1.3: {} mlly@1.8.2: @@ -5459,8 +6088,12 @@ snapshots: nanoid@3.3.17: {} + napi-postinstall@0.3.4: {} + natural-compare@1.4.0: {} + neo-async@2.6.2: {} + node-releases@2.0.53: {} nwsapi@2.2.24: {} @@ -5560,6 +6193,8 @@ snapshots: picocolors@1.1.1: {} + picomatch@2.3.2: {} + picomatch@4.0.3: {} pkg-types@1.3.1: @@ -5651,6 +6286,15 @@ snapshots: resolve-from@4.0.0: {} + resolve-pkg-maps@1.0.0: {} + + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + resolve@2.0.0-next.5: dependencies: is-core-module: 2.16.1 @@ -5823,6 +6467,10 @@ snapshots: source-map-js@1.2.1: {} + source-map@0.6.1: {} + + stable-hash-x@0.2.0: {} + stackback@0.0.2: {} std-env@3.10.0: {} @@ -5910,6 +6558,27 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + svelte@5.56.9(@typescript-eslint/types@8.50.0): + dependencies: + '@jridgewell/remapping': 2.3.5 + '@jridgewell/sourcemap-codec': 1.5.5 + '@sveltejs/acorn-typescript': 1.0.13(acorn@8.18.0) + '@types/estree': 1.0.9 + '@types/trusted-types': 2.0.7 + acorn: 8.18.0 + aria-query: 5.3.1 + axobject-query: 4.1.0 + clsx: 2.1.1 + devalue: 5.9.0 + esm-env: 1.2.2 + esrap: 2.3.4(@typescript-eslint/types@8.50.0) + is-reference: 3.0.3 + locate-character: 3.0.0 + magic-string: 0.30.21 + zimmerframe: 1.1.4 + transitivePeerDependencies: + - '@typescript-eslint/types' + symbol-tree@3.2.4: {} tailwind-merge@3.6.0: {} @@ -5945,6 +6614,10 @@ snapshots: dependencies: tldts-core: 6.1.86 + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + tough-cookie@5.1.2: dependencies: tldts: 6.1.86 @@ -6021,6 +6694,9 @@ snapshots: ufo@1.6.4: {} + uglify-js@3.19.3: + optional: true + unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 @@ -6063,6 +6739,33 @@ snapshots: picomatch: 4.0.3 webpack-virtual-modules: 0.6.2 + unrs-resolver@1.12.2: + dependencies: + napi-postinstall: 0.3.4 + optionalDependencies: + '@unrs/resolver-binding-android-arm-eabi': 1.12.2 + '@unrs/resolver-binding-android-arm64': 1.12.2 + '@unrs/resolver-binding-darwin-arm64': 1.12.2 + '@unrs/resolver-binding-darwin-x64': 1.12.2 + '@unrs/resolver-binding-freebsd-x64': 1.12.2 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.12.2 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.12.2 + '@unrs/resolver-binding-linux-arm64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-arm64-musl': 1.12.2 + '@unrs/resolver-binding-linux-loong64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-loong64-musl': 1.12.2 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-riscv64-musl': 1.12.2 + '@unrs/resolver-binding-linux-s390x-gnu': 1.12.2 + '@unrs/resolver-binding-linux-x64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-x64-musl': 1.12.2 + '@unrs/resolver-binding-openharmony-arm64': 1.12.2 + '@unrs/resolver-binding-wasm32-wasi': 1.12.2 + '@unrs/resolver-binding-win32-arm64-msvc': 1.12.2 + '@unrs/resolver-binding-win32-ia32-msvc': 1.12.2 + '@unrs/resolver-binding-win32-x64-msvc': 1.12.2 + update-browserslist-db@1.3.1(browserslist@4.28.8): dependencies: browserslist: 4.28.8 @@ -6123,6 +6826,10 @@ snapshots: jiti: 2.7.0 lightningcss: 1.32.0 + vitefu@1.1.3(vite@6.4.3(@types/node@22.15.3)(jiti@2.7.0)(lightningcss@1.32.0)): + optionalDependencies: + vite: 6.4.3(@types/node@22.15.3)(jiti@2.7.0)(lightningcss@1.32.0) + vitest@3.2.7(@types/node@22.15.3)(jiti@2.7.0)(jsdom@26.1.0)(lightningcss@1.32.0): dependencies: '@types/chai': 5.2.3 @@ -6254,6 +6961,8 @@ snapshots: word-wrap@1.2.5: {} + wordwrap@1.0.0: {} + workerd@1.20260801.1: optionalDependencies: '@cloudflare/workerd-darwin-64': 1.20260801.1 @@ -6322,3 +7031,5 @@ snapshots: '@speed-highlight/core': 1.2.24 cookie: 1.1.1 youch-core: 0.3.3 + + zimmerframe@1.1.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 2b70329..d6de7d7 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -18,6 +18,8 @@ catalogs: 'typescript': '5.9.2' 'vite': '^6.1.0' 'vite-plugin-dts': '^5.0.3' + 'svelte': '^5.0.0' + '@sveltejs/vite-plugin-svelte': '^5.0.0' 'vue': '^3.5.0' 'vue-tsc': '^2.2.0' 'vitest': '^3.0.7'