diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..02f2612 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,24 @@ +# EditorConfig — 跨编辑器统一代码风格 +# https://editorconfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = crlf +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false + +[*.{rs,toml}] +indent_size = 4 + +[*.{yml,yaml}] +indent_size = 2 + +[Makefile] +indent_style = tab diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..43b96ad --- /dev/null +++ b/.gitattributes @@ -0,0 +1,32 @@ +# Git 行尾符规范化 +# 本仓库统一 CRLF(Windows 原生项目) + +# 源码文本文件 +*.ts text eol=crlf +*.tsx text eol=crlf +*.js text eol=crlf +*.json text eol=crlf +*.html text eol=crlf +*.css text eol=crlf +*.md text eol=crlf +*.rs text eol=crlf +*.toml text eol=crlf +*.yml text eol=crlf +*.yaml text eol=crlf +*.svg text eol=crlf +*.txt text eol=crlf +*.editorconfig text eol=crlf +*.gitattributes text eol=crlf +*.gitignore text eol=crlf +LICENSE text eol=crlf + +# 二进制文件 +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +*.pdf binary +*.dll binary +*.exe binary +*.nsis binary diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..0497638 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,20 @@ +# 代码所有者 — 自动分配 PR 审查 +# https://docs.github.com/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners + +# 全局所有者 +* @LHY0125 + +# Rust 代码 +/core/ @LHY0125 +/cli/ @LHY0125 +/gui/ @LHY0125 +/Cargo.toml @LHY0125 +/rust-toolchain.toml @LHY0125 + +# 前端代码 +/src/ @LHY0125 +/tests/ @LHY0125 +/e2e/ @LHY0125 + +# CI/CD 和配置文件 +/.github/ @LHY0125 diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..67edefb --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,5 @@ +# 开源赞助 +# 支持 PathEditor 的开发 + +github: LHY0125 +# 如需定制功能或商业授权,请通过 GitHub Issues 联系 diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..bec6d24 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,73 @@ +# Dependabot 自动依赖更新配置 +# https://docs.github.com/code-security/dependabot/dependabot-version-updates + +version: 2 +updates: + # npm 前端依赖 + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "09:00" + timezone: "Asia/Shanghai" + versioning-strategy: "auto" + allow: + - dependency-type: "all" + labels: + - "dependencies" + - "javascript" + commit-message: + prefix: "chore(deps)" + prefix-development: "chore(deps-dev)" + open-pull-requests-limit: 5 + groups: + react: + patterns: + - "react" + - "react-dom" + - "@types/react" + - "@types/react-dom" + tauri: + patterns: + - "@tauri-apps/*" + testing: + patterns: + - "@testing-library/*" + - "@playwright/test" + - "vitest" + - "jsdom" + eslint: + patterns: + - "eslint" + - "eslint-plugin-*" + - "typescript-eslint" + - "globals" + - "@eslint/js" + + # Cargo Rust 依赖 + - package-ecosystem: "cargo" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "09:00" + timezone: "Asia/Shanghai" + labels: + - "dependencies" + - "rust" + commit-message: + prefix: "chore(deps)" + prefix-development: "chore(deps-dev)" + open-pull-requests-limit: 3 + + # GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + labels: + - "dependencies" + - "ci" + commit-message: + prefix: "ci(deps)" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 430fe29..ef90ef7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ permissions: jobs: frontend: - name: 前端检查 (TypeScript + Lint + Test) + name: 前端检查 (格式 + 类型 + Lint + 测试 + 覆盖率) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -24,23 +24,37 @@ jobs: - run: npm ci + - name: Prettier 格式检查 + run: npx prettier --check "src/**/*.{ts,tsx}" "tests/**/*.{ts,tsx}" "e2e/**/*.ts" + - name: TypeScript 类型检查 run: npx tsc -b --noEmit - name: ESLint run: npx eslint src/ tests/ e2e/ - - name: Vitest 测试 - run: npm test + - name: Vitest 测试 + 覆盖率 + run: npx vitest run --coverage + + - name: 上传覆盖率到 Codecov + uses: codecov/codecov-action@v5 + with: + files: ./coverage/cobertura-coverage.xml + flags: frontend + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} rust: - name: Rust 检查 (Check + Clippy + Test) + name: Rust 检查 (格式 + Check + Clippy + Test) runs-on: windows-latest steps: - uses: actions/checkout@v4 - uses: Swatinem/rust-cache@v2 + - name: Cargo Format Check + run: cargo fmt --check + - name: Cargo Check run: cargo check @@ -49,6 +63,3 @@ jobs: - name: Cargo Test run: cargo test - - - name: Cargo Format Check - run: cargo fmt --check diff --git a/.gitignore b/.gitignore index b24c0f3..e22172e 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,18 @@ dist dist-ssr *.local +# Coverage +coverage/ +*.lcov + +# Sync conflicts +*.sync-conflict-* + +# Test artifacts +test-results/ +playwright-report/ +.nyc_output/ + # Editor directories and files .vscode/* !.vscode/extensions.json @@ -22,9 +34,17 @@ dist-ssr *.njsproj *.sln *.sw? + +# AI assistant .claude/ .codegraph/ CLAUDE.md + +# Platform e2e/debug-screenshot.png -test-results/ target/ + +# Archive +*.zip +*.7z +*.tar.gz diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100644 index 0000000..0a4b97d --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1 @@ +npx --no -- commitlint --edit $1 diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..2312dc5 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +npx lint-staged diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..2588642 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,8 @@ +{ + "default": true, + "MD013": false, + "MD033": { + "allowed_elements": ["img", "br", "kbd", "summary", "details"] + }, + "MD041": false +} diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..3880956 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,10 @@ +node_modules +dist +dist-ssr +target +*.local +*.log +test-results +coverage +Cargo.lock +package-lock.json diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..6dd41b5 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,10 @@ +{ + "semi": true, + "singleQuote": true, + "trailingComma": "all", + "printWidth": 100, + "tabWidth": 2, + "endOfLine": "crlf", + "arrowParens": "always", + "bracketSpacing": true +} diff --git a/Cargo.lock b/Cargo.lock index 2643e23..53cf21e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2386,6 +2386,7 @@ dependencies = [ "log", "serde", "serde_json", + "windows-sys 0.61.2", "winreg 0.52.0", ] diff --git a/README.md b/README.md index 957cbeb..436b7b8 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,33 @@ typescript license tests + coverage + platform CI

--- +## 截图 + +### 主界面 + +![主界面](docs/screenshots/main-window.png) + +### 路径编辑 + +![路径编辑](docs/screenshots/path-edit.png) + +### 冲突检测 + +![冲突检测](docs/screenshots/conflict-analysis.png) + +### CLI 命令行 + +![CLI](docs/screenshots/cli-demo.png) + +--- + ## 简介 PathEditor 是 Windows PATH 环境变量的可视化管理工具。支持系统变量和用户变量的增删改查、拖拽排序、一键清理无效路径、导入导出以及完整的撤销/重做。 @@ -251,18 +273,18 @@ npx tauri build ### 技术栈 -| 层 | 技术 | -|---|---| -| 前端框架 | React 19 + TypeScript (strict) | -| UI 样式 | Tailwind CSS 4 | -| 状态管理 | Zustand | -| 国际化 | i18next | -| 桌面框架 | Tauri 2.x | -| 核心库 | Rust workspace (core + gui + cli) | -| 前端测试 | Vitest (100 个测试) | -| Rust 测试 | cargo test (57 个测试) | -| 构建 | Vite + Cargo | -| 打包 | NSIS | +| 层 | 技术 | +| --------- | --------------------------------- | +| 前端框架 | React 19 + TypeScript (strict) | +| UI 样式 | Tailwind CSS 4 | +| 状态管理 | Zustand | +| 国际化 | i18next | +| 桌面框架 | Tauri 2.x | +| 核心库 | Rust workspace (core + gui + cli) | +| 前端测试 | Vitest (100 个测试) | +| Rust 测试 | cargo test (57 个测试) | +| 构建 | Vite + Cargo | +| 打包 | NSIS | ### 项目结构 @@ -291,15 +313,15 @@ docs/ # 审查文档 ## 快捷键 -| 快捷键 | 功能 | -|--------|------| +| 快捷键 | 功能 | +| -------- | -------- | | `Ctrl+N` | 新建路径 | -| `Ctrl+S` | 保存 | -| `Ctrl+Z` | 撤销 | -| `Ctrl+Y` | 重做 | -| `Ctrl+F` | 搜索 | +| `Ctrl+S` | 保存 | +| `Ctrl+Z` | 撤销 | +| `Ctrl+Y` | 重做 | +| `Ctrl+F` | 搜索 | | `Delete` | 删除选中 | -| `F1` | 帮助 | +| `F1` | 帮助 | ## 贡献 diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..29b8af0 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,48 @@ +# 路线图 + +PathEditor 的未来发展方向。 + +## v5.1 (下一个版本) + +- [ ] **CLI 模块化** — `cli/src/main.rs` 拆分为 `commands/` 子模块 +- [ ] **自动更新** — 内置 Tauri updater,无需手动下载安装包 +- [ ] **深色模式优化** — 对齐 Windows 系统主题自动切换 +- [ ] **性能优化** — 虚拟滚动支持超长 PATH 列表(1000+ 条目) + +## v5.2 + +- [ ] **PATH 历史快照** — 保存每次修改的时间线,支持回退到任意历史节点 +- [ ] **规则引擎** — 自定义 PATH 整理规则(如「所有 Python 路径放最前」) +- [ ] **收藏夹** — 常用路径快速添加 +- [ ] **冲突解决方案引导** — 可视化的可执行文件冲突对比与解决建议 + +## v6.0 (长期) + +- [ ] **跨平台支持** — 适配 Linux (`/etc/environment` + `~/.profile`) 和 macOS (`path_helper`) +- [ ] **Web 管理面板** — 远程管理多台 Windows 服务器的 PATH 环境变量 +- [ ] **插件系统** — 第三方扩展生态(如 Anaconda/VSCode/VS 自动检测与配置) +- [ ] **Windows Package Manager 集成** — 与 winget/chocolatey 联动,检测包管理器安装的路径 + +## 已交付 + +### v5.0.0 + +- ✅ Cargo workspace 三层架构 (core + gui + cli) +- ✅ CLI 命令行工具 (18 条命令) +- ✅ 冲突检测 + 工具清单 +- ✅ 配置文件管理 +- ✅ 撤销/重做 (10 种操作) +- ✅ 中英双语界面 +- ✅ CI/CD 自动化 + +### v4.x 系列 + +- ✅ Tauri 2.x 重写 +- ✅ 路径验证 (红色/橙色标记) +- ✅ 导入/导出 JSON/CSV/TXT +- ✅ 深色/浅色模式 +- ✅ 全局键盘快捷键 + +--- + +欢迎通过 [Issues](https://github.com/LHY0125/PathEditor/issues) 提交功能建议! diff --git a/SUPPORT.md b/SUPPORT.md new file mode 100644 index 0000000..0edf910 --- /dev/null +++ b/SUPPORT.md @@ -0,0 +1,45 @@ +# 获取帮助 + +## 📖 文档 + +- [README](README.md) — 项目简介、功能列表、安装指南 +- [CONTRIBUTING](CONTRIBUTING.md) — 贡献指南 +- [CHANGELOG](CHANGELOG.md) — 版本变更记录 +- [ROADMAP](ROADMAP.md) — 未来规划 +- [SECURITY](SECURITY.md) — 安全政策 + +## 🐛 报告 Bug + +1. 先搜索 [Issues](https://github.com/LHY0125/PathEditor/issues) 确认未被报告 +2. 使用 **Bug Report** 模板创建新 Issue +3. 提供系统信息(Windows 版本、PathEditor 版本) +4. 附上复现步骤和截图 + +## 💡 功能建议 + +1. 检查 [ROADMAP](ROADMAP.md) 确认不在已有计划中 +2. 使用 **Feature Request** 模板创建新 Issue +3. 描述使用场景和期望行为 + +## ❓ 常见问题 + +### CLI 命令找不到? + +```bash +patheditor --help +``` + +确保已通过 `cargo install --path cli` 安装,且 `~/.cargo/bin` 在 PATH 中。 + +### 提示权限不足? + +编辑系统 PATH 需要管理员权限。右键以管理员身份运行,或使用 CLI `patheditor check-admin` 检测。 + +### 保存后环境变量未生效? + +PathEditor 会自动广播 `WM_SETTINGCHANGE`,但部分程序需要手动重启才能识别新 PATH。 + +## 📧 联系 + +- GitHub Issues: [LHY0125/PathEditor](https://github.com/LHY0125/PathEditor/issues) +- 安全问题: 参见 [SECURITY.md](SECURITY.md) diff --git a/commitlint.config.js b/commitlint.config.js new file mode 100644 index 0000000..c4c9714 --- /dev/null +++ b/commitlint.config.js @@ -0,0 +1,12 @@ +/** @type {import('@commitlint/types').UserConfig} */ +export default { + extends: ['@commitlint/config-conventional'], + rules: { + 'type-enum': [ + 2, + 'always', + ['feat', 'fix', 'refactor', 'docs', 'test', 'chore', 'perf', 'ci', 'style', 'revert'], + ], + 'subject-case': [0], + }, +}; diff --git a/core/Cargo.toml b/core/Cargo.toml index 8370a58..872688b 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -13,3 +13,4 @@ log = "0.4" winreg = "0.52" dirs = "5" chrono = "0.4" +windows-sys = { version = "0.61.2", features = ["Win32_System_Environment", "Win32_UI_WindowsAndMessaging", "Win32_Foundation"] } diff --git a/core/src/system.rs b/core/src/system.rs index a5da4af..aef4e12 100644 --- a/core/src/system.rs +++ b/core/src/system.rs @@ -1,3 +1,7 @@ +use windows_sys::Win32::System::Environment::ExpandEnvironmentStringsW; +use windows_sys::Win32::UI::WindowsAndMessaging::{ + SendMessageTimeoutW, HWND_BROADCAST, SMTO_ABORTIFHUNG, WM_SETTINGCHANGE, +}; use winreg::enums::*; use winreg::RegKey; @@ -26,6 +30,7 @@ pub fn validate_path(path: &str) -> bool { } /// 展开路径中的环境变量(如 %JAVA_HOME%\bin → C:\Program Files\Java\jdk-17\bin) +/// 包含 % 的路径(环境变量路径)无法展开,返回原始路径 pub fn expand_env_vars(path: &str) -> String { if !path.contains('%') { return path.to_string(); @@ -46,7 +51,7 @@ pub fn expand_env_vars(path: &str) -> String { // SAFETY: buffer 容量为 required(API 返回的精确大小),wide_path 以 null 结尾, // 且两个指针指向不同的内存区域,不存在重叠 - let mut buffer: Vec = vec![0; required as usize]; + let mut buffer = vec![0_u16; required as usize]; let result = unsafe { ExpandEnvironmentStringsW(wide_path.as_ptr(), buffer.as_mut_ptr(), required) }; @@ -73,10 +78,6 @@ fn decode_utf16_preserving(v: &[u16]) -> String { /// 广播环境变量更改通知(WM_SETTINGCHANGE) /// 广播 `WM_SETTINGCHANGE` 通知系统环境变量已变更 pub fn broadcast_env_change() { - const HWND_BROADCAST: isize = 0xFFFF; - const WM_SETTINGCHANGE: u32 = 0x001A; - const SMTO_ABORTIFHUNG: u32 = 0x0002; - // SAFETY: env_str 是以 null 结尾的 UTF-16 字符串,所有指针和常量均遵循 Win32 API 约定 let env_str: Vec = "Environment\0".encode_utf16().collect(); @@ -84,7 +85,7 @@ pub fn broadcast_env_change() { // lpdwResult 为 null 表示不需要返回值,其他参数均为常量 let result = unsafe { SendMessageTimeoutW( - HWND_BROADCAST, + HWND_BROADCAST as _, WM_SETTINGCHANGE, 0, env_str.as_ptr() as isize, @@ -101,24 +102,6 @@ pub fn broadcast_env_change() { } } -// ── 外部 FFI 声明 ── - -extern "system" { - /// https://learn.microsoft.com/en-us/windows/win32/api/processenv/nf-processenv-expandenvironmentstringsw - fn ExpandEnvironmentStringsW(lpSrc: *const u16, lpDst: *mut u16, nSize: u32) -> u32; - - /// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-sendmessagetimeoutw - fn SendMessageTimeoutW( - hWnd: isize, - Msg: u32, - wParam: usize, - lParam: isize, - fuFlags: u32, - uTimeout: u32, - lpdwResult: *mut usize, - ) -> isize; -} - #[cfg(test)] mod tests { use super::*; diff --git a/docs/code-and-architecture-review.md b/docs/code-and-architecture-review.md new file mode 100644 index 0000000..c7ebe53 --- /dev/null +++ b/docs/code-and-architecture-review.md @@ -0,0 +1,69 @@ +# PathEditor v5.0 代码与架构审查报告 + +## 1. 项目概览 + +PathEditor v5.0 是一个功能完善的 Windows 系统环境变量 (PATH) 编辑器,支持 GUI 与 CLI 双模式。 +技术栈选型现代化且合理: +- **后端 / 核心逻辑**:Rust (Cargo Workspace) +- **GUI 框架**:Tauri 2.x +- **前端**:React 19 + TypeScript + Zustand + +整体项目结构清晰,职责划分明确,严格遵循了前后端分离与核心逻辑无平台依赖的设计原则。 + +## 2. 架构设计审查 + +### 2.1 Cargo Workspace 三层架构 +项目采用了经典的 Cargo Workspace 模式,分为三层: +- `core`: 纯 Rust 库 crate,包含所有的核心业务逻辑(注册表读写、备份、配置文件管理、路径验证与清理等)。该层**完全不依赖** Tauri 或 CLI 库,极大地提高了代码的复用性和可测试性。 +- `gui`: Tauri 桌面应用。仅作为薄包装层(Thin Wrapper),通过 `#[tauri::command]` 将 `core` 的功能暴露为 IPC 接口供前端调用。 +- `cli`: 命令行工具层。依赖 `core` 和 `clap` 库,直接提供命令行交互能力。 + +**审查结论**:架构设计非常优秀。核心逻辑解耦彻底,无论是 GUI 还是 CLI 都能复用同一套安全、经过测试的核心代码。 + +### 2.2 IPC 通信与状态同步 +前端与 Rust 后端通过 Tauri IPC 进行通信。 +- 所有的错误处理均通过 `Result` 返回,前端通过 `Promise` 捕获并处理,用户体验良好。 +- 针对非事务性的双写操作(如同时保存系统和用户 PATH),前端 `app-store.ts` 中使用了 `Promise.allSettled`。当发生部分成功(Partial Success)时,能正确捕获并重新加载注册表状态,避免了前端内存状态与后端注册表状态的漂移(State Drift)。 + +## 3. 后端代码审查 (Rust) + +### 3.1 核心逻辑 (`core`) +- **安全性与健壮性**: + - 在 `registry.rs` 中,严格检查了路径字符串的 Null 字节,以及 32767 个字符的 Windows 注册表长度上限,防止缓冲区溢出或写入失败。 + - 使用了安全的 `winreg` 库进行注册表操作。 +- **FFI 调用**: + - 在 `system.rs` 中调用 Windows API(如 `ExpandEnvironmentStringsW` 和 `SendMessageTimeoutW`)时,对 `unsafe` 代码块进行了详尽的 SAFETY 注释。 + - 能够妥善处理 UTF-16 编码和解码,保留非法码点避免丢失路径信息,细节处理非常到位。 + +### 3.2 命令行工具 (`cli`) +- **原子性与并发安全**: + - 在 CLI 的 `verify_and_save` 逻辑中,写入前会重新读取注册表并与原始状态对比。如果不一致,则拒绝写入并报错退出。这有效地防止了并发情况下的配置覆盖问题。 +- **用户体验**: + - 命令设计符合直觉,支持 `--dry-run` 预览以及 JSON 格式输出,方便与其他脚本集成。 + +## 4. 前端代码审查 (React + TypeScript) + +### 4.1 状态管理 (`app-store.ts`) +- 使用 `Zustand` 进行全局状态管理,状态树设计合理,避免了 React Context 可能带来的不必要重渲染。 +- 实现了完善的 `UndoRedoManager`,将每一步操作抽象为 `OperationType`,支持撤销/重做功能,这对于编辑器类应用来说是核心体验的加分项。 +- `isSaving` 状态守卫有效防止了用户双击保存按钮引发的并发竞争。 + +### 4.2 UI 与逻辑分离 +- 业务逻辑抽象到 `src/core` 目录下(如 `path-manager.ts`, `validation.ts`),UI 组件仅负责渲染和事件绑定。 +- `useAppActions.ts` 钩子巧妙地将组件层与 Store 状态操作解耦,使得组件代码极其整洁。 + +## 5. 改进建议 (Recommendations) + +虽然当前代码质量已经很高,但仍有以下几个方面可以进一步优化: + +1. **Rust FFI 维护性**: + 当前 `system.rs` 中手动声明了 `extern "system"` 函数。建议引入 `windows-rs` 或 `windows-sys` 库,这能提供微软官方维护的安全的 API 绑定,减少手动编写 FFI 签名带来的维护成本和潜在错误。 +2. **GUI 保存的并发安全 (Race Condition)**: + CLI 已经实现了保存前的二次状态比对(`verify_and_save`),但在 `gui/src/commands/registry.rs` 中,直接调用了 `save_system_paths`。如果在用户打开 GUI 修改期间,另一个进程修改了注册表,GUI 保存时可能会覆盖该修改。建议在 GUI 的 IPC 保存接口中,也引入类似 CLI 的版本校验(例如传入 `expected_original_paths` 进行比对)。 +3. **前端单元测试覆盖**: + 核心逻辑如 `undo-redo.ts` 和 `path-manager.ts` 纯函数特性明显,建议在 `tests/unit/` 下增加对这些文件的边界用例测试,确保复杂编辑操作下状态不崩溃。 +4. **长列表性能**: + 如果 PATH 环境变量条目非常多(虽然实际场景中一般在 100 条以内),React 渲染完整列表可能会有微小延迟。当前规模下无影响,但若未来考虑显示大量工具链路径扫描结果,可引入虚拟列表(Virtual List)。 + +## 总结 +PathEditor v5.0 的代码库是一个优秀的 Rust + Tauri + React 实践范例。它具有清晰的三层架构、严格的类型和边界检查、以及良好的错误处理机制,整体架构稳健且易于长期维护。 diff --git a/docs/screenshots/.gitkeep b/docs/screenshots/.gitkeep new file mode 100644 index 0000000..a44d07a --- /dev/null +++ b/docs/screenshots/.gitkeep @@ -0,0 +1 @@ +# 截图目录 diff --git a/docs/screenshots/cli-demo.png b/docs/screenshots/cli-demo.png new file mode 100644 index 0000000..48ea000 Binary files /dev/null and b/docs/screenshots/cli-demo.png differ diff --git a/docs/screenshots/conflict-analysis.png b/docs/screenshots/conflict-analysis.png new file mode 100644 index 0000000..7ce4493 Binary files /dev/null and b/docs/screenshots/conflict-analysis.png differ diff --git a/docs/screenshots/main-window.png b/docs/screenshots/main-window.png new file mode 100644 index 0000000..4212d51 Binary files /dev/null and b/docs/screenshots/main-window.png differ diff --git a/docs/screenshots/path-edit.png b/docs/screenshots/path-edit.png new file mode 100644 index 0000000..ec8303a Binary files /dev/null and b/docs/screenshots/path-edit.png differ diff --git a/e2e/tests/search-clean.spec.ts b/e2e/tests/search-clean.spec.ts index b37b5f7..461d4c5 100644 --- a/e2e/tests/search-clean.spec.ts +++ b/e2e/tests/search-clean.spec.ts @@ -2,11 +2,13 @@ import { test, expect } from '@playwright/test'; import { createIpcMock } from '../mocks/ipc'; test.beforeEach(async ({ page }) => { - await page.addInitScript(createIpcMock({ - load_system_paths: ['C:\\Windows', 'invalid_path', 'C:\\Temp'], - load_user_paths: [], - validate_path: false, - })); + await page.addInitScript( + createIpcMock({ + load_system_paths: ['C:\\Windows', 'invalid_path', 'C:\\Temp'], + load_user_paths: [], + validate_path: false, + }), + ); await page.goto('/'); }); diff --git a/gui/src/commands/registry.rs b/gui/src/commands/registry.rs index b38c93e..0e7001d 100644 --- a/gui/src/commands/registry.rs +++ b/gui/src/commands/registry.rs @@ -9,10 +9,22 @@ pub fn load_user_paths() -> Result, String> { registry::load_user_paths() } #[tauri::command] -pub fn save_system_paths(paths: Vec) -> Result<(), String> { +pub fn save_system_paths(paths: Vec, original: Option>) -> Result<(), String> { + if let Some(orig) = original { + let current = registry::load_system_paths()?; + if current != orig { + return Err("注册表已被其他进程修改,请重新加载后重试".to_string()); + } + } registry::save_system_paths(paths) } #[tauri::command] -pub fn save_user_paths(paths: Vec) -> Result<(), String> { +pub fn save_user_paths(paths: Vec, original: Option>) -> Result<(), String> { + if let Some(orig) = original { + let current = registry::load_user_paths()?; + if current != orig { + return Err("注册表已被其他进程修改,请重新加载后重试".to_string()); + } + } registry::save_user_paths(paths) } diff --git a/index.html b/index.html index 6333e4f..2b48773 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,7 @@ - PathEditor v4.0 + PathEditor v5.1
diff --git a/package-lock.json b/package-lock.json index 42a046b..566df7c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,15 @@ { "name": "patheditor", - "version": "5.0.0", + "version": "5.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "patheditor", - "version": "5.0.0", + "version": "5.1.0", "dependencies": { "@tailwindcss/vite": "^4.3.0", + "@tanstack/react-virtual": "^3.13.26", "@tauri-apps/api": "^2.11.0", "@tauri-apps/plugin-dialog": "^2.7.1", "i18next": "^26.2.0", @@ -20,9 +21,12 @@ "zustand": "^5.0.13" }, "devDependencies": { + "@commitlint/cli": "^21.0.2", + "@commitlint/config-conventional": "^21.0.2", "@eslint/js": "^10.0.1", "@playwright/test": "^1.60.0", "@tauri-apps/cli": "^2.11.2", + "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@types/node": "^24.12.3", "@types/react": "^19.2.14", @@ -33,13 +37,23 @@ "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.6.0", + "husky": "^9.1.7", "jsdom": "^29.1.1", + "lint-staged": "^16.4.0", + "prettier": "^3.8.4", "typescript": "~6.0.2", "typescript-eslint": "^8.59.2", "vite": "^8.0.12", "vitest": "^4.1.7" } }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@asamuzakjp/css-color": { "version": "5.1.11", "resolved": "https://registry.npmmirror.com/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", @@ -363,6 +377,333 @@ "specificity": "bin/cli.js" } }, + "node_modules/@commitlint/cli": { + "version": "21.0.2", + "resolved": "https://registry.npmmirror.com/@commitlint/cli/-/cli-21.0.2.tgz", + "integrity": "sha512-YMmfLbqBg+ZRvvmPhc+cilSQFrh/AgzVgCT1U/OifmUZEwPbvCtA8rN//YNaF9d5eoZphxVMGYtmwA2QgQORgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/format": "^21.0.1", + "@commitlint/lint": "^21.0.2", + "@commitlint/load": "^21.0.2", + "@commitlint/read": "^21.0.2", + "@commitlint/types": "^21.0.1", + "tinyexec": "^1.0.0", + "yargs": "^18.0.0" + }, + "bin": { + "commitlint": "cli.js" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@commitlint/config-conventional": { + "version": "21.0.2", + "resolved": "https://registry.npmmirror.com/@commitlint/config-conventional/-/config-conventional-21.0.2.tgz", + "integrity": "sha512-P/ZRhryQmkj0Z0dY9FOoRwe3xkwJyyAdtXwt01NT2kuZttcG2CNYp1q5Ci3u+nDT2jcbJRw2kt13Czl1qKNPfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^21.0.1", + "conventional-changelog-conventionalcommits": "^9.2.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@commitlint/config-validator": { + "version": "21.0.1", + "resolved": "https://registry.npmmirror.com/@commitlint/config-validator/-/config-validator-21.0.1.tgz", + "integrity": "sha512-Zd2UFdndeMMaW2O96HK0tdfT4gOImUvidMpAd/pws2zZ4m1nrAZ/9b/v2JYuE8fs86GpXv9F7LNaIuCIWhY+pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^21.0.1", + "ajv": "^8.11.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@commitlint/config-validator/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@commitlint/config-validator/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@commitlint/ensure": { + "version": "21.0.1", + "resolved": "https://registry.npmmirror.com/@commitlint/ensure/-/ensure-21.0.1.tgz", + "integrity": "sha512-jJ1037967wU7YN/xkv+iRlOBlmaOXPhPO5KQSqya6GyXzBlwuLzELBFao16DVg9dZyqmNrhewzwZ3SAibetHBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^21.0.1", + "es-toolkit": "^1.46.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@commitlint/execute-rule": { + "version": "21.0.1", + "resolved": "https://registry.npmmirror.com/@commitlint/execute-rule/-/execute-rule-21.0.1.tgz", + "integrity": "sha512-RifH+FmImozKBE6mozhF4K3r2RRKP7SMi/Q/zLCmExtp5e05lhHOUYqGBlFBAGNHaZxU/WYw1XuugYK9jQzqnA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@commitlint/format": { + "version": "21.0.1", + "resolved": "https://registry.npmmirror.com/@commitlint/format/-/format-21.0.1.tgz", + "integrity": "sha512-ksmG2+cHGtuDPQQbhBbC4unwm444+6TiPw0d1bKf67hntgZqZ8E0g1MuYKUuyT5IH4IMmXZhKq22/Z3jBvtQIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^21.0.1", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@commitlint/is-ignored": { + "version": "21.0.2", + "resolved": "https://registry.npmmirror.com/@commitlint/is-ignored/-/is-ignored-21.0.2.tgz", + "integrity": "sha512-H5z4t8PC9tUsmZ/o+EptM3Nq8sTFtskAShdcqxCoyzklW5eaVT5xbrDAET2uypzir9Vsj4ZZmBtyKjYe2XqgeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^21.0.1", + "semver": "^7.6.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@commitlint/is-ignored/node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@commitlint/lint": { + "version": "21.0.2", + "resolved": "https://registry.npmmirror.com/@commitlint/lint/-/lint-21.0.2.tgz", + "integrity": "sha512-PnUmLYGeGLfW8oVatR9KpNxSHYAnJOEWlMZzfdeFOUq6WUrFx1fGQaWCWJqMoIll/xPM+GdfJV+tKHZVHhl0Fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/is-ignored": "^21.0.2", + "@commitlint/parse": "^21.0.2", + "@commitlint/rules": "^21.0.2", + "@commitlint/types": "^21.0.1" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@commitlint/load": { + "version": "21.0.2", + "resolved": "https://registry.npmmirror.com/@commitlint/load/-/load-21.0.2.tgz", + "integrity": "sha512-lwUE70hN0/qE/ZRROhbaX65ly/FF12DrqfReLCESo37M0OQCFAf2jRS+2tSCSORq+bm4Kdju7qNDj46uc1QzTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/config-validator": "^21.0.1", + "@commitlint/execute-rule": "^21.0.1", + "@commitlint/resolve-extends": "^21.0.1", + "@commitlint/types": "^21.0.1", + "cosmiconfig": "^9.0.1", + "cosmiconfig-typescript-loader": "^6.1.0", + "es-toolkit": "^1.46.0", + "is-plain-obj": "^4.1.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@commitlint/message": { + "version": "21.0.2", + "resolved": "https://registry.npmmirror.com/@commitlint/message/-/message-21.0.2.tgz", + "integrity": "sha512-5n4aqHGD/FNnom/D5L8i7cYtV+xjuXcBL832C3w9VglEsZzIsoHpJsvxzJ7cgiOsOdc/2jU4t5+7qMHh7GBX3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@commitlint/parse": { + "version": "21.0.2", + "resolved": "https://registry.npmmirror.com/@commitlint/parse/-/parse-21.0.2.tgz", + "integrity": "sha512-QVZJhGHTm+oiuWyEKOCTQ0ZM3mfJ0eGWFeHuj7WzSKEth+UukcCHac9GD8pgdFlg/qGkFWOtyaNd1T8REgagaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^21.0.1", + "conventional-changelog-angular": "^8.2.0", + "conventional-commits-parser": "^6.3.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@commitlint/read": { + "version": "21.0.2", + "resolved": "https://registry.npmmirror.com/@commitlint/read/-/read-21.0.2.tgz", + "integrity": "sha512-BtsrnLVycSSKf4Q0gMch4giCj5NNlmcbhc8ra5vONgGtP2IjRDo33bEFtr5Pm+2N+5fXGWb2MksWPrspPfdhdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/top-level": "^21.0.2", + "@commitlint/types": "^21.0.1", + "git-raw-commits": "^5.0.0", + "tinyexec": "^1.0.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@commitlint/resolve-extends": { + "version": "21.0.1", + "resolved": "https://registry.npmmirror.com/@commitlint/resolve-extends/-/resolve-extends-21.0.1.tgz", + "integrity": "sha512-0DhjYWL6uYrY16Efa032fYk3woGJDU4AGWiG1XXltT9AMUNYKyb5cIZU2ivbaMZ3+kKFqUjikD2cjh66Sbh/Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/config-validator": "^21.0.1", + "@commitlint/types": "^21.0.1", + "es-toolkit": "^1.46.0", + "global-directory": "^5.0.0", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@commitlint/rules": { + "version": "21.0.2", + "resolved": "https://registry.npmmirror.com/@commitlint/rules/-/rules-21.0.2.tgz", + "integrity": "sha512-k6tQ69Td7t2qUSIbik8D3TL1q3ZJpkEbV+yLogDzCRAdOxJm4ndhtBNREsLA1/puRfWvzS9eioF2w43WT+hHgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/ensure": "^21.0.1", + "@commitlint/message": "^21.0.2", + "@commitlint/to-lines": "^21.0.1", + "@commitlint/types": "^21.0.1" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@commitlint/to-lines": { + "version": "21.0.1", + "resolved": "https://registry.npmmirror.com/@commitlint/to-lines/-/to-lines-21.0.1.tgz", + "integrity": "sha512-bd1BFII7p1EQZre9Kaj+kKaMFP3cFCdt21K7DItVux9XP5WjLgJ0/Uy1pJJh9aPwVJ6SKg62PxqlZaHI8hQAXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@commitlint/top-level": { + "version": "21.0.2", + "resolved": "https://registry.npmmirror.com/@commitlint/top-level/-/top-level-21.0.2.tgz", + "integrity": "sha512-s9KKM+e+mXgFeIh4n7KmOGAVT3mkJ3Fp1bBYHIK5pjeUwlEMzp/tZfb5u0Poa680AsQTXMEMRxZi1vQ9m2X5ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@commitlint/types": { + "version": "21.0.1", + "resolved": "https://registry.npmmirror.com/@commitlint/types/-/types-21.0.1.tgz", + "integrity": "sha512-4u7w8jcoCUFWhjWnASYzZHAP34OqOtuFBN87nQmFvqda03YU0T6z+yB4w0gSAMpekiRqqGk5rt+qSlW+a2vSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "conventional-commits-parser": "^6.3.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@conventional-changelog/git-client": { + "version": "2.7.0", + "resolved": "https://registry.npmmirror.com/@conventional-changelog/git-client/-/git-client-2.7.0.tgz", + "integrity": "sha512-j7A8/LBEQ+3rugMzPXoKYzyUPpw/0CBQCyvtTR7Lmu4olG4yRC/Tfkq79Mr3yuPs0SUitlO2HwGP3gitMJnRFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@simple-libs/child-process-utils": "^1.0.0", + "@simple-libs/stream-utils": "^1.2.0", + "semver": "^7.5.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "conventional-commits-filter": "^5.0.0", + "conventional-commits-parser": "^6.4.0" + }, + "peerDependenciesMeta": { + "conventional-commits-filter": { + "optional": true + }, + "conventional-commits-parser": { + "optional": true + } + } + }, + "node_modules/@conventional-changelog/git-client/node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@csstools/color-helpers": { "version": "6.0.2", "resolved": "https://registry.npmmirror.com/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", @@ -1082,6 +1423,35 @@ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "license": "MIT" }, + "node_modules/@simple-libs/child-process-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/@simple-libs/child-process-utils/-/child-process-utils-1.0.2.tgz", + "integrity": "sha512-/4R8QKnd/8agJynkNdJmNw2MBxuFTRcNFnE5Sg/G+jkSsV8/UBgULMzhizWWW42p8L5H7flImV2ATi79Ove2Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@simple-libs/stream-utils": "^1.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://ko-fi.com/dangreen" + } + }, + "node_modules/@simple-libs/stream-utils": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/@simple-libs/stream-utils/-/stream-utils-1.2.0.tgz", + "integrity": "sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://ko-fi.com/dangreen" + } + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -1346,6 +1716,33 @@ "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, + "node_modules/@tanstack/react-virtual": { + "version": "3.13.26", + "resolved": "https://registry.npmmirror.com/@tanstack/react-virtual/-/react-virtual-3.13.26.tgz", + "integrity": "sha512-DosdgjOxCLahkn0o+ilmZYwEjo1glfMGuRT/j3PQ18yr5XqA8N/BCaL9IJ3B5TRl+nnzyK2IOFgAILwzN3a9xQ==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.16.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.16.0", + "resolved": "https://registry.npmmirror.com/@tanstack/virtual-core/-/virtual-core-3.16.0.tgz", + "integrity": "sha512-Er2N7q3WOiH6y2JLxsxNX+u2/sLqSsL0bxFgDjuiPiA7vKhZRm+IzcS17vRee3GNXr64UsesA5CAp9yTiIYw9A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tauri-apps/api": { "version": "2.11.0", "resolved": "https://registry.npmmirror.com/@tauri-apps/api/-/api-2.11.0.tgz", @@ -1603,6 +2000,33 @@ "node": ">=18" } }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmmirror.com/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, "node_modules/@testing-library/react": { "version": "16.3.2", "resolved": "https://registry.npmmirror.com/@testing-library/react/-/react-16.3.2.tgz", @@ -2171,6 +2595,22 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmmirror.com/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -2196,17 +2636,30 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, "node_modules/aria-query": { "version": "5.3.0", "resolved": "https://registry.npmmirror.com/aria-query/-/aria-query-5.3.0.tgz", "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "dequal": "^2.0.3" } }, + "node_modules/array-ify": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/array-ify/-/array-ify-1.0.0.tgz", + "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==", + "dev": true, + "license": "MIT" + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmmirror.com/assertion-error/-/assertion-error-2.0.1.tgz", @@ -2316,6 +2769,16 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001793", "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", @@ -2347,6 +2810,143 @@ "node": ">=18" } }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/cli-truncate/-/cli-truncate-5.2.0.tgz", + "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^8.0.0", + "string-width": "^8.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmmirror.com/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmmirror.com/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmmirror.com/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/compare-func": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/compare-func/-/compare-func-2.0.0.tgz", + "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-ify": "^1.0.0", + "dot-prop": "^5.1.0" + } + }, + "node_modules/conventional-changelog-angular": { + "version": "8.3.1", + "resolved": "https://registry.npmmirror.com/conventional-changelog-angular/-/conventional-changelog-angular-8.3.1.tgz", + "integrity": "sha512-6gfI3otXK5Ph5DfCOI1dblr+kN3FAm5a97hYoQkqNZxOaYa5WKfXH+AnpsmS+iUH2mgVC2Cg2Qw9m5OKcmNrIg==", + "dev": true, + "license": "ISC", + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/conventional-changelog-conventionalcommits": { + "version": "9.3.1", + "resolved": "https://registry.npmmirror.com/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-9.3.1.tgz", + "integrity": "sha512-dTYtpIacRpcZgrvBYvBfArMmK2xvIpv2TaxM0/ZI5CBtNUzvF2x0t15HsbRABWprS6UPmvj+PzHVjSx4qAVKyw==", + "dev": true, + "license": "ISC", + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/conventional-commits-parser": { + "version": "6.4.0", + "resolved": "https://registry.npmmirror.com/conventional-commits-parser/-/conventional-commits-parser-6.4.0.tgz", + "integrity": "sha512-tvRg7FIBNlyPzjdG8wWRlPHQJJHI7DylhtRGeU9Lq+JuoPh5BKpPRX83ZdLrvXuOSu5Eo/e7SzOQhU4Hd2Miuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@simple-libs/stream-utils": "^1.2.0", + "meow": "^13.0.0" + }, + "bin": { + "conventional-commits-parser": "dist/cli/index.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -2354,6 +2954,61 @@ "dev": true, "license": "MIT" }, + "node_modules/cosmiconfig": { + "version": "9.0.2", + "resolved": "https://registry.npmmirror.com/cosmiconfig/-/cosmiconfig-9.0.2.tgz", + "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cosmiconfig-typescript-loader": { + "version": "6.3.0", + "resolved": "https://registry.npmmirror.com/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-6.3.0.tgz", + "integrity": "sha512-Akr82WH1Wfqatyiqpj8HDkO2o2KmJRu1FhKfSNJP3K4IdXwHfEyL7MOb62i1AGQVLtIQM+iCE9CGOtrfhR+mmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jiti": "2.6.1" + }, + "engines": { + "node": ">=v18" + }, + "peerDependencies": { + "@types/node": "*", + "cosmiconfig": ">=9", + "typescript": ">=5" + } + }, + "node_modules/cosmiconfig-typescript-loader/node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmmirror.com/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2383,6 +3038,13 @@ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" } }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", @@ -2442,7 +3104,6 @@ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6" } @@ -2464,6 +3125,19 @@ "license": "MIT", "peer": true }, + "node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.361", "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.361.tgz", @@ -2471,6 +3145,13 @@ "dev": true, "license": "ISC" }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, "node_modules/enhanced-resolve": { "version": "5.22.0", "resolved": "https://registry.npmmirror.com/enhanced-resolve/-/enhanced-resolve-5.22.0.tgz", @@ -2497,6 +3178,39 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmmirror.com/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmmirror.com/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-module-lexer": { "version": "2.1.0", "resolved": "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-2.1.0.tgz", @@ -2504,6 +3218,17 @@ "dev": true, "license": "MIT" }, + "node_modules/es-toolkit": { + "version": "1.47.1", + "resolved": "https://registry.npmmirror.com/es-toolkit/-/es-toolkit-1.47.1.tgz", + "integrity": "sha512-5RAqEwf4P4E17p+W75KLOWw/nOvKZzSQpxM32IpI2KZLaVonjTrZ0Ai5ghMaVI9eKC2p8eoQgcBdkEDgzFk6+Q==", + "dev": true, + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", @@ -2719,6 +3444,13 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true, + "license": "MIT" + }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmmirror.com/expect-type/-/expect-type-1.3.0.tgz", @@ -2750,6 +3482,23 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", @@ -2842,6 +3591,46 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/git-raw-commits": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/git-raw-commits/-/git-raw-commits-5.0.1.tgz", + "integrity": "sha512-Y+csSm2GD/PCSh6Isd/WiMjNAydu0VBiG9J7EdQsNA5P9uXvLayqjmTsNlK5Gs9IhblFZqOU0yid5Il5JPoLiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@conventional-changelog/git-client": "^2.6.0", + "meow": "^13.0.0" + }, + "bin": { + "git-raw-commits": "src/cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz", @@ -2855,6 +3644,22 @@ "node": ">=10.13.0" } }, + "node_modules/global-directory": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/global-directory/-/global-directory-5.0.0.tgz", + "integrity": "sha512-1pgFdhK3J2LeM+dVf2Pd424yHx2ou338lC0ErNP2hPx4j8eW1Sp0XqSjNxtk6Tc4Kr5wlWtSvz8cn2yb7/SG/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "6.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/globals": { "version": "17.6.0", "resolved": "https://registry.npmmirror.com/globals/-/globals-17.6.0.tgz", @@ -2930,6 +3735,22 @@ "void-elements": "3.1.0" } }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmmirror.com/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/i18next": { "version": "26.2.0", "resolved": "https://registry.npmmirror.com/i18next/-/i18next-26.2.0.tgz", @@ -2977,6 +3798,33 @@ "node": ">= 4" } }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -2987,6 +3835,33 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ini": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/ini/-/ini-6.0.0.tgz", + "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2997,6 +3872,22 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", @@ -3010,6 +3901,29 @@ "node": ">=0.10.0" } }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -3079,6 +3993,29 @@ "dev": true, "license": "MIT" }, + "node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/jsdom": { "version": "29.1.1", "resolved": "https://registry.npmmirror.com/jsdom/-/jsdom-29.1.1.tgz", @@ -3150,6 +4087,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -3450,6 +4394,55 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lint-staged": { + "version": "16.4.0", + "resolved": "https://registry.npmmirror.com/lint-staged/-/lint-staged-16.4.0.tgz", + "integrity": "sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^14.0.3", + "listr2": "^9.0.5", + "picomatch": "^4.0.3", + "string-argv": "^0.3.2", + "tinyexec": "^1.0.4", + "yaml": "^2.8.2" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/listr2": { + "version": "9.0.5", + "resolved": "https://registry.npmmirror.com/listr2/-/listr2-9.0.5.tgz", + "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^5.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz", @@ -3466,6 +4459,56 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", @@ -3544,6 +4587,42 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/meow": { + "version": "13.2.0", + "resolved": "https://registry.npmmirror.com/meow/-/meow-13.2.0.tgz", + "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "10.2.5", "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.5.tgz", @@ -3613,6 +4692,22 @@ ], "license": "MIT" }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz", @@ -3663,6 +4758,38 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parse5": { "version": "8.0.1", "resolved": "https://registry.npmmirror.com/parse5/-/parse5-8.0.1.tgz", @@ -3806,6 +4933,22 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.8.4", + "resolved": "https://registry.npmmirror.com/prettier/-/prettier-3.8.4.tgz", + "integrity": "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmmirror.com/pretty-format/-/pretty-format-27.5.1.tgz", @@ -3888,6 +5031,20 @@ "license": "MIT", "peer": true }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz", @@ -3898,6 +5055,40 @@ "node": ">=0.10.0" } }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, "node_modules/rolldown": { "version": "1.0.2", "resolved": "https://registry.npmmirror.com/rolldown/-/rolldown-1.0.2.tgz", @@ -3990,6 +5181,49 @@ "dev": true, "license": "ISC" }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slice-ansi": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-8.0.0.tgz", + "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", @@ -4013,6 +5247,75 @@ "dev": true, "license": "MIT" }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmmirror.com/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "8.2.1", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-8.2.1.tgz", + "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", @@ -4546,6 +5849,55 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/xml-name-validator": { "version": "5.0.0", "resolved": "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz", @@ -4563,6 +5915,16 @@ "dev": true, "license": "MIT" }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", @@ -4570,6 +5932,68 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "devOptional": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmmirror.com/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 933827a..838a3ab 100644 --- a/package.json +++ b/package.json @@ -3,20 +3,33 @@ "private": true, "version": "5.1.0", "type": "module", + "lint-staged": { + "*.{ts,tsx}": [ + "prettier --write", + "eslint --fix" + ], + "*.{json,md,css,html}": [ + "prettier --write" + ] + }, "scripts": { "dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", "lint:fix": "eslint . --fix", - "format": "cargo fmt", + "format": "prettier --write \"src/**/*.{ts,tsx}\" \"tests/**/*.{ts,tsx}\" \"e2e/**/*.ts\"", + "format:check": "prettier --check \"src/**/*.{ts,tsx}\" \"tests/**/*.{ts,tsx}\" \"e2e/**/*.ts\"", + "format:rust": "cargo fmt", "preview": "vite preview", "test": "vitest run", "test:watch": "vitest", "test:coverage": "vitest run --coverage", - "test:e2e": "playwright test --config e2e/playwright.config.ts" + "test:e2e": "playwright test --config e2e/playwright.config.ts", + "prepare": "husky" }, "dependencies": { "@tailwindcss/vite": "^4.3.0", + "@tanstack/react-virtual": "^3.13.26", "@tauri-apps/api": "^2.11.0", "@tauri-apps/plugin-dialog": "^2.7.1", "i18next": "^26.2.0", @@ -28,9 +41,12 @@ "zustand": "^5.0.13" }, "devDependencies": { + "@commitlint/cli": "^21.0.2", + "@commitlint/config-conventional": "^21.0.2", "@eslint/js": "^10.0.1", "@playwright/test": "^1.60.0", "@tauri-apps/cli": "^2.11.2", + "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@types/node": "^24.12.3", "@types/react": "^19.2.14", @@ -41,7 +57,10 @@ "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.6.0", + "husky": "^9.1.7", "jsdom": "^29.1.1", + "lint-staged": "^16.4.0", + "prettier": "^3.8.4", "typescript": "~6.0.2", "typescript-eslint": "^8.59.2", "vite": "^8.0.12", diff --git a/src/components/dialogs/AnalyzeDialog.tsx b/src/components/dialogs/AnalyzeDialog.tsx index 3dc5565..a00f862 100644 --- a/src/components/dialogs/AnalyzeDialog.tsx +++ b/src/components/dialogs/AnalyzeDialog.tsx @@ -37,7 +37,10 @@ export function AnalyzeDialog({ open, onClose }: Props) { const prevOpen = useRef(false); useEffect(() => { - if (!open) { prevOpen.current = false; return; } + if (!open) { + prevOpen.current = false; + return; + } if (prevOpen.current) return; prevOpen.current = true; setLoading(true); @@ -67,7 +70,10 @@ export function AnalyzeDialog({ open, onClose }: Props) {
{/* 标题栏 */} -
+

{t('analyze.title')}

{(['conflicts', 'tools'] as TabType[]).map((tb) => ( @@ -89,7 +95,10 @@ export function AnalyzeDialog({ open, onClose }: Props) { {/* 内容 */}
{loading ? ( -
+
{t('analyze.scanning')}
) : tab === 'conflicts' ? ( @@ -215,5 +224,7 @@ function EmptyHint({ text }: { text: string }) { function getEnabledPaths(): string[] { const { sysPaths, userPaths } = useAppStore.getState(); - return [...sysPaths.filter((e) => e.enabled), ...userPaths.filter((e) => e.enabled)].map((e) => e.path); + return [...sysPaths.filter((e) => e.enabled), ...userPaths.filter((e) => e.enabled)].map( + (e) => e.path, + ); } diff --git a/src/components/dialogs/HelpDialog.tsx b/src/components/dialogs/HelpDialog.tsx index 7428d6e..9602216 100644 --- a/src/components/dialogs/HelpDialog.tsx +++ b/src/components/dialogs/HelpDialog.tsx @@ -1,7 +1,10 @@ import { useTranslation } from 'react-i18next'; import { Modal } from '@/components/ui/Modal'; -interface HelpDialogProps { open: boolean; onClose: () => void; } +interface HelpDialogProps { + open: boolean; + onClose: () => void; +} export function HelpDialog({ open, onClose }: HelpDialogProps) { const { t } = useTranslation(); @@ -9,9 +12,15 @@ export function HelpDialog({ open, onClose }: HelpDialogProps) { return (

{t('dialog.helpTitle')}

-
{t('help.content')}
+
+        {t('help.content')}
+      
-
diff --git a/src/components/dialogs/ImportDialog.tsx b/src/components/dialogs/ImportDialog.tsx index f6a0679..985030c 100644 --- a/src/components/dialogs/ImportDialog.tsx +++ b/src/components/dialogs/ImportDialog.tsx @@ -9,7 +9,13 @@ interface ImportDialogProps { onCancel: () => void; } -export function ImportDialog({ open, systemCount, userCount, onSelect, onCancel }: ImportDialogProps) { +export function ImportDialog({ + open, + systemCount, + userCount, + onSelect, + onCancel, +}: ImportDialogProps) { const { t } = useTranslation(); return ( @@ -21,10 +27,40 @@ export function ImportDialog({ open, systemCount, userCount, onSelect, onCancel {userCount > 0 && t('dialog.importUserCount', { count: userCount })}

- {systemCount > 0 && } - {userCount > 0 && } - {systemCount > 0 && userCount > 0 && } - + {systemCount > 0 && ( + + )} + {userCount > 0 && ( + + )} + {systemCount > 0 && userCount > 0 && ( + + )} +
); diff --git a/src/components/dialogs/PathEditDialog.tsx b/src/components/dialogs/PathEditDialog.tsx index d31e17f..acb33af 100644 --- a/src/components/dialogs/PathEditDialog.tsx +++ b/src/components/dialogs/PathEditDialog.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { Modal } from '@/components/ui/Modal'; @@ -10,30 +10,54 @@ interface PathEditDialogProps { onCancel: () => void; } -export function PathEditDialog({ open, title, initialValue, onConfirm, onCancel }: PathEditDialogProps) { +export function PathEditDialog({ + open, + title, + initialValue, + onConfirm, + onCancel, +}: PathEditDialogProps) { const { t } = useTranslation(); const [value, setValue] = useState(initialValue); + const prevOpen = useRef(open); - // 对话框打开时重置输入值 — 此模式不会导致级联渲染 - // eslint-disable-next-line react-hooks/set-state-in-effect - useEffect(() => { if (open) setValue(initialValue); }, [open, initialValue]); + useEffect(() => { + if (open && !prevOpen.current) setValue(initialValue); + prevOpen.current = open; + }, [open, initialValue]); return (

{title}

setValue(e.target.value)} - onKeyDown={(e) => { if (e.key === 'Enter') onConfirm(value); }} + onKeyDown={(e) => { + if (e.key === 'Enter') onConfirm(value); + }} className="w-full min-w-[400px] px-3 py-2 rounded border text-sm outline-none" - style={{ backgroundColor: 'var(--app-list-bg)', color: 'var(--app-fg)', borderColor: 'var(--app-border)' }} + style={{ + backgroundColor: 'var(--app-list-bg)', + color: 'var(--app-fg)', + borderColor: 'var(--app-border)', + }} />
- -
diff --git a/src/components/dialogs/ProfileDialog.tsx b/src/components/dialogs/ProfileDialog.tsx index fc2a028..fc61c57 100644 --- a/src/components/dialogs/ProfileDialog.tsx +++ b/src/components/dialogs/ProfileDialog.tsx @@ -65,22 +65,39 @@ export function ProfileDialog({ open, onClose }: Props) { if (!selected || !selectedData) return; if (!window.confirm(t('profile.applyConfirm', { name: selected }))) return; useAppStore.getState().replaceBothPaths( - selectedData.sys.map(e => e.path), - selectedData.user.map(e => e.path), + selectedData.sys.map((e) => e.path), + selectedData.user.map((e) => e.path), ); // 同步 disabled 状态 await invoke('save_disabled_state', { - system: selectedData.sys.filter(e => !e.enabled).map(e => e.path), - user: selectedData.user.filter(e => !e.enabled).map(e => e.path), + system: selectedData.sys.filter((e) => !e.enabled).map((e) => e.path), + user: selectedData.user.filter((e) => !e.enabled).map((e) => e.path), }); - await useAppStore.getState().savePaths(); - onClose(); + const result = await useAppStore.getState().savePaths(); + if (result.kind === 'success') { + onClose(); + } else if (result.kind === 'warning') { + const { ask } = await import('@tauri-apps/plugin-dialog'); + const confirmed = await ask(t('status.saveWarningLongPaths'), { + title: t('dialog.backupTitle'), + kind: 'warning', + }); + if (confirmed) { + const forceResult = await useAppStore.getState().savePaths(true); + if (forceResult.kind === 'success') { + onClose(); + } + } + } }; const handleDelete = async (name: string) => { if (!window.confirm(`删除配置文件 "${name}"?`)) return; await invoke('delete_profile', { name }); - if (selected === name) { setSelected(null); setSelectedData(null); } + if (selected === name) { + setSelected(null); + setSelectedData(null); + } refreshProfiles(); }; @@ -95,16 +112,23 @@ export function ProfileDialog({ open, onClose }: Props) { return (
-
+

{t('profile.title')}

setNewName(e.target.value)} + onChange={(e) => setNewName(e.target.value)} placeholder={t('profile.namePlaceholder')} className="px-2 py-1 text-sm rounded border outline-none w-44" - style={{ backgroundColor: 'var(--app-list-bg)', color: 'var(--app-fg)', borderColor: 'var(--app-border)' }} + style={{ + backgroundColor: 'var(--app-list-bg)', + color: 'var(--app-fg)', + borderColor: 'var(--app-border)', + }} /> @@ -189,18 +223,32 @@ export function ProfileDialog({ open, onClose }: Props) { setRenameValue(e.target.value)} + onChange={(e) => setRenameValue(e.target.value)} className="px-2 py-1 text-xs rounded border outline-none" - style={{ backgroundColor: 'var(--app-list-bg)', color: 'var(--app-fg)', borderColor: 'var(--app-border)' }} + style={{ + backgroundColor: 'var(--app-list-bg)', + color: 'var(--app-fg)', + borderColor: 'var(--app-border)', + }} /> -
)} - - + +
)}
@@ -214,9 +262,13 @@ function PathSection({ title, paths }: { title: string; paths: PathEntry[] }) { const { t } = useTranslation(); return (
-
{title}
+
+ {title} +
{paths.length === 0 ? ( -
{t('profile.empty')}
+
+ {t('profile.empty')} +
) : (
{paths.map((e) => ( diff --git a/src/components/layout/AppShell.tsx b/src/components/layout/AppShell.tsx index abfb008..c41e67d 100644 --- a/src/components/layout/AppShell.tsx +++ b/src/components/layout/AppShell.tsx @@ -28,19 +28,32 @@ export function AppShell() { const setSelectedIndices = useAppStore((s) => s.setSelectedIndices); const [editDialog, setEditDialog] = useState({ - open: false, index: -1, value: '', target: TargetType.SYSTEM, + open: false, + index: -1, + value: '', + target: TargetType.SYSTEM, }); const [newDialog, setNewDialog] = useState(false); const [helpOpen, setHelpOpen] = useState(false); const [importDialog, setImportDialog] = useState({ - open: false, system: [], user: [], + open: false, + system: [], + user: [], }); const [analyzeOpen, setAnalyzeOpen] = useState(false); const [profilesOpen, setProfilesOpen] = useState(false); const actions = useAppActions(activeTab, { - editDialog, newDialog, helpOpen, importDialog, - setEditDialog, setNewDialog, setHelpOpen, setImportDialog, setAnalyzeOpen, setProfilesOpen, + editDialog, + newDialog, + helpOpen, + importDialog, + setEditDialog, + setNewDialog, + setHelpOpen, + setImportDialog, + setAnalyzeOpen, + setProfilesOpen, }); const tabConfig: { id: TabId; label: string }[] = [ @@ -50,14 +63,20 @@ export function AppShell() { ]; return ( -
+
{tabConfig.map((tab) => (
diff --git a/src/components/layout/ErrorBoundary.tsx b/src/components/layout/ErrorBoundary.tsx index 3aa9a0f..941b614 100644 --- a/src/components/layout/ErrorBoundary.tsx +++ b/src/components/layout/ErrorBoundary.tsx @@ -1,7 +1,12 @@ import { Component, type ReactNode } from 'react'; -interface Props { children: ReactNode; } -interface State { hasError: boolean; error: string; } +interface Props { + children: ReactNode; +} +interface State { + hasError: boolean; + error: string; +} export class ErrorBoundary extends Component { state: State = { hasError: false, error: '' }; @@ -18,7 +23,10 @@ export class ErrorBoundary extends Component { render() { if (this.state.hasError) { return ( -
+

应用出错

{this.state.error}

diff --git a/src/components/layout/TitleBar.tsx b/src/components/layout/TitleBar.tsx index 5a3f1dc..b2f2129 100644 --- a/src/components/layout/TitleBar.tsx +++ b/src/components/layout/TitleBar.tsx @@ -11,9 +11,7 @@ export function TitleBar() { className="flex items-center justify-between px-4 py-2 border-b select-none" style={{ borderColor: 'var(--app-border)' }} > -

- {isAdmin ? t('app.name') : t('app.nameReadonly')} -

+

{isAdmin ? t('app.name') : t('app.nameReadonly')}

v{version} ); diff --git a/src/components/path-list/MergePreview.tsx b/src/components/path-list/MergePreview.tsx index d57d32a..af7b47c 100644 --- a/src/components/path-list/MergePreview.tsx +++ b/src/components/path-list/MergePreview.tsx @@ -1,7 +1,8 @@ -import { useMemo } from 'react'; +import { useMemo, useRef } from 'react'; import { useAppStore } from '@/store/app-store'; import { useTranslation } from 'react-i18next'; import type { PathEntry } from '@/core/path-entry'; +import { useVirtualizer } from '@tanstack/react-virtual'; export function MergePreview() { const sysPaths = useAppStore((s) => s.sysPaths); @@ -33,47 +34,62 @@ export function MergePreview() { return merged.filter((r) => r.path.toLowerCase().includes(q)); }, [sysPaths, userPaths, searchQuery, t]); - return ( -
- - - - - - - - - - {allPaths.map(({ path, enabled, source, displayIndex }, rowIdx) => { - const textColor = enabled ? 'var(--app-fg)' : '#6b7280'; - const textDecoration = enabled ? 'none' : 'line-through'; - const opacity = enabled ? 1 : 0.6; + const parentRef = useRef(null); - return ( - parentRef.current, + estimateSize: () => 28, // 预估行高 28px + initialRect: { width: 800, height: 600 }, + }); + + return ( +
+
+
#
+
{t('dialog.pathLabel')}
+
{t('merge.source')}
+
+
+ {rowVirtualizer.getVirtualItems().map((virtualRow) => { + const rowIdx = virtualRow.index; + const { path, enabled, source, displayIndex } = allPaths[rowIdx]; + const textColor = enabled ? 'var(--app-fg)' : '#6b7280'; + const textDecoration = enabled ? 'none' : 'line-through'; + const opacity = enabled ? 1 : 0.6; + + return ( +
+
{rowIdx + 1}
+
-
- - - - ); - })} - -
#{t('dialog.pathLabel')}{t('merge.source')}
{rowIdx + 1} - {path} - {source}
+ {path} +
+
{source}
+
+ ); + })} +
); } diff --git a/src/components/path-list/PathTable.tsx b/src/components/path-list/PathTable.tsx index 74a0125..992b75f 100644 --- a/src/components/path-list/PathTable.tsx +++ b/src/components/path-list/PathTable.tsx @@ -1,9 +1,10 @@ -import { useMemo, useCallback } from 'react'; +import { useMemo, useCallback, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { useAppStore } from '@/store/app-store'; import { TargetType } from '@/core/undo-redo'; import { usePathValidation } from '@/hooks/use-path-validation'; import type { ValidationState } from '@/hooks/use-path-validation'; +import { useVirtualizer } from '@tanstack/react-virtual'; interface PathTableProps { tabId: 'system' | 'user'; @@ -36,7 +37,8 @@ export function PathTable({ tabId }: PathTableProps) { const result: PathRow[] = []; for (let i = 0; i < paths.length; i++) { const p = paths[i]; - if (p.path.toLowerCase().includes(q)) result.push({ path: p.path, index: i, enabled: p.enabled }); + if (p.path.toLowerCase().includes(q)) + result.push({ path: p.path, index: i, enabled: p.enabled }); } return result; }, [paths, searchQuery]); @@ -80,76 +82,94 @@ export function PathTable({ tabId }: PathTableProps) { [isActive, paths], ); + const parentRef = useRef(null); + + const rowVirtualizer = useVirtualizer({ + count: filtered.length, + getScrollElement: () => parentRef.current, + estimateSize: () => 28, // 预估行高 28px + initialRect: { width: 800, height: 600 }, + }); + return ( -
- - - - - - - - - - {filtered.map(({ path, index, enabled }, rowIdx) => { - const v = validations[rowIdx]; - const isSelected = selectedIndices.includes(index); - let textColor = 'var(--app-fg)'; - if (v.state === 'invalid') textColor = '#dc3545'; - else if (v.isDuplicate) textColor = '#fd7e14'; - else if (v.state === 'unknown') textColor = 'var(--app-fg)'; +
+
+
#
+
+
{t('table.path')}
+
+
+ {rowVirtualizer.getVirtualItems().map((virtualRow) => { + const rowIdx = virtualRow.index; + const { path, index, enabled } = filtered[rowIdx]; + const v = validations[rowIdx]; + const isSelected = selectedIndices.includes(index); + let textColor = 'var(--app-fg)'; + if (v.state === 'invalid') textColor = '#dc3545'; + else if (v.isDuplicate) textColor = '#fd7e14'; + else if (v.state === 'unknown') textColor = 'var(--app-fg)'; - let textDecoration = 'none'; - let opacity = 1; - if (!enabled) { - textColor = '#6b7280'; - textDecoration = 'line-through'; - opacity = 0.6; - } + let textDecoration = 'none'; + let opacity = 1; + if (!enabled) { + textColor = '#6b7280'; + textDecoration = 'line-through'; + opacity = 0.6; + } - return ( -
handleClick(index, e)} - onDoubleClick={() => handleDoubleClick(index)} - className="cursor-pointer select-none" - style={{ - backgroundColor: isSelected - ? 'var(--app-select-row)' - : rowIdx % 2 === 0 - ? 'var(--app-list-bg)' - : 'var(--app-list-alt)', - }} + return ( +
handleClick(index, e)} + onDoubleClick={() => handleDoubleClick(index)} + className="cursor-pointer select-none flex items-center absolute top-0 left-0 w-full" + style={{ + height: `${virtualRow.size}px`, + transform: `translateY(${virtualRow.start}px)`, + backgroundColor: isSelected + ? 'var(--app-select-row)' + : rowIdx % 2 === 0 + ? 'var(--app-list-bg)' + : 'var(--app-list-alt)', + }} + > +
-
- - - - ); - })} - -
#{t('table.path')}
- {index + 1} - - { - const target = tabId === 'system' ? TargetType.SYSTEM : TargetType.USER; - useAppStore.getState().togglePath(index, target); - }} - className="cursor-pointer" - /> - - {path} -
+ {index + 1} +
+
+ { + const target = tabId === 'system' ? TargetType.SYSTEM : TargetType.USER; + useAppStore.getState().togglePath(index, target); + }} + className="cursor-pointer" + /> +
+
+ {path} +
+
+ ); + })} +
); } diff --git a/src/components/toolbar/ToolBar.tsx b/src/components/toolbar/ToolBar.tsx index f45b412..4964054 100644 --- a/src/components/toolbar/ToolBar.tsx +++ b/src/components/toolbar/ToolBar.tsx @@ -36,12 +36,7 @@ export function ToolBar(props: ToolBarProps) {
-