mirror of
https://github.com/LHY0125/PathEditor.git
synced 2026-06-29 01:45:54 +08:00
chore: 全面代码审查修复 + 开源标配完善
## 审查修复 (18 项)
- TitleBar 版本号改为动态 import package.json
- CLI profile_apply 加 verify_and_save 原子性保护
- CLI 新增 profile rename 子命令
- cmd_clean 默认清理 system+user 两个 hive
- Rust import_csv 加 BOM/header 处理
- exportToJson/exportToCsv 保留 enabled 状态
- CLI version 使用 env!("CARGO_PKG_VERSION")
- export_paths 返回 Result, 未知格式报错
- importFromContent 未知扩展名 throw Error
- profile 文件名加路径遍历/Win保留字校验
- 数据路径统一到 ~/.patheditor/
## clippy (18 处修复)
- backup/scanner/system/profiles: empty_line_after_doc_comments
- profiles: needless_borrow ×5, unnecessary_map_or
- scanner: collapsible_if
- cli: nonminimal_bool ×6, implicit_saturating_sub, to_string_in_format_args
- 零警告通过
## 测试 (33 条新增)
- Rust: backup(3) + disabled(1) + fs(13) + scanner(4) + profiles(1) = 25 条
- 前端: merge-preview(2) + analyze-dialog(1) + import-parity(5) = 8 条
- Rust 10→35, 前端 72→80
## Scanner 并行化
- std::thread::scope 多线程并行扫描目录,N 倍性能提升
## expand_env_vars UTF-16 修复
- 非法码点编码为 \u{XXXX} 而非静默丢弃
## 开源标配
- CODE_OF_CONDUCT.md (Contributor Covenant 2.1)
- SECURITY.md (漏洞报告流程)
- .github/PULL_REQUEST_TEMPLATE.md
- CONTRIBUTING.md (贡献指南)
- CHANGELOG.md (v4.0~v5.0)
## E2E 测试 (4 条新增)
- keyboard / analyze / profiles / import-export
- IPC mock 扩展 scan/profiles 命令
## CI
- Rust job 目录调整为 workspace 根
## 其他
- rustdoc: 8 个 pub fn 补文档注释
- 帮助文本 v4.0→v5.0
- 前后端导入逻辑加交叉引用注释
- .gitignore 添加 target/
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,28 @@
|
|||||||
|
## 改动描述
|
||||||
|
|
||||||
|
<!-- 简要描述此 PR 做了什么 -->
|
||||||
|
|
||||||
|
## 关联 Issue
|
||||||
|
|
||||||
|
<!-- 关闭 #123 -->
|
||||||
|
|
||||||
|
## 改动类型
|
||||||
|
|
||||||
|
- [ ] 新功能 (feat)
|
||||||
|
- [ ] Bug 修复 (fix)
|
||||||
|
- [ ] 重构 (refactor)
|
||||||
|
- [ ] 文档 (docs)
|
||||||
|
- [ ] 测试 (test)
|
||||||
|
- [ ] 构建/CI (chore)
|
||||||
|
|
||||||
|
## 测试计划
|
||||||
|
|
||||||
|
- [ ] `cargo clippy -- -D warnings` 零警告
|
||||||
|
- [ ] `cargo test` 全部通过
|
||||||
|
- [ ] `npm test` 全部通过
|
||||||
|
- [ ] `npx tsc -b --noEmit` 零错误
|
||||||
|
- [ ] E2E 测试 (如有 UI 变更)
|
||||||
|
|
||||||
|
## 截图
|
||||||
|
|
||||||
|
<!-- 如涉及 UI 改动,请附上前后对比截图 -->
|
||||||
@@ -36,9 +36,6 @@ jobs:
|
|||||||
rust:
|
rust:
|
||||||
name: Rust 检查 (Check + Clippy + Test)
|
name: Rust 检查 (Check + Clippy + Test)
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: gui
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
|||||||
@@ -26,3 +26,4 @@ dist-ssr
|
|||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
e2e/debug-screenshot.png
|
e2e/debug-screenshot.png
|
||||||
test-results/
|
test-results/
|
||||||
|
target/
|
||||||
|
|||||||
+43
-66
@@ -1,77 +1,54 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [4.2.0] — 2026-05-28
|
## 5.0.0 (2026-05-29)
|
||||||
|
|
||||||
### 新增
|
### Added
|
||||||
- 路径启用/禁用功能:复选框控制 PATH 中每条路径是否生效
|
- Cargo workspace 三层架构 (core + gui + cli)
|
||||||
- PathEntry 数据类型:替代原有 `string[]`,支持 `enabled` 状态
|
- CLI 命令行工具,17 条命令,支持 JSON 输出
|
||||||
- `disabled.json` 持久化禁用状态的独立存储
|
- PATH 可执行文件冲突检测 (`scan_conflicts`)
|
||||||
- E2E 测试框架:Playwright + 4 条核心流程测试
|
- PATH 目录工具清单 (`scan_tools`)
|
||||||
- CI/CD 流水线:TypeScript + Rust 自动检查,Release 自动构建
|
- 配置文件管理:保存/加载/应用/重命名/删除
|
||||||
|
- 系统+用户合并预览视图
|
||||||
|
- CLI 原子性保护:写入前重新读取注册表对比
|
||||||
|
- `--steps N` 参数支持多格移动 (CLI 特有)
|
||||||
|
|
||||||
### 修复
|
### Changed
|
||||||
- undo/redo after toggle 未持久化 disabled 状态
|
- Rust + Tauri 2.x + React 19 + TypeScript strict 全重写
|
||||||
- expand_env_vars 两次 API 调用间缓冲区截断风险
|
- 撤销/重做系统扩展至 10 种操作类型
|
||||||
- E2E mock load_disabled_state 返回格式与 Rust 后端不匹配
|
- 禁用状态即时持久化,不依赖保存按钮
|
||||||
- 双 hive 保存失败时错误信息只显示一个
|
- 深色模式 / 浅色模式 CSS 变量驱动
|
||||||
- 导入 both 产生两条 undo 记录,需两次 Ctrl+Z
|
- 中英双语界面 (i18next)
|
||||||
- 备份失败警告被"保存成功"覆盖
|
- 备份文件存储路径统一到 `~/.patheditor/`
|
||||||
- 非连续多行删除后 undo 恢复到错误位置
|
- 版本号集中管理: Rust 端 `Cargo.toml` workspace, 前端 `package.json`
|
||||||
- backup_registry 未 await 导致竞态保存新值
|
|
||||||
|
|
||||||
### 变更
|
### Fixed
|
||||||
- 导入改用原生文件对话框(`@tauri-apps/plugin-dialog`)
|
- 非管理员自动进入只读模式
|
||||||
- PathTable 环境变量展开限流 20 并发
|
- 保存失败精确提示哪个注册表 hive 出错 (Promise.allSettled)
|
||||||
- CI 切换到 MSVC 工具链
|
- CLI `--system`/`--user` 互斥校验
|
||||||
- 版本号统一为 4.2.0
|
- 修改操作后广播 `WM_SETTINGCHANGE`
|
||||||
|
- 深色模式下行选中颜色对比度不足
|
||||||
|
- 窗口内容溢出无法滚动
|
||||||
|
|
||||||
---
|
## 4.2.0
|
||||||
|
|
||||||
## [4.1.0] — 2026-05-26
|
### Fixed
|
||||||
|
- Release workflow 兼容已存在的 release
|
||||||
|
|
||||||
### 新增
|
## 4.1.0
|
||||||
- app-store 单元测试:25 个测试覆盖 CRUD/undo-redo/loadPaths/savePaths
|
|
||||||
- 72 个前端单元测试 + 10 个 Rust 单元测试
|
|
||||||
|
|
||||||
### 修复
|
### Added
|
||||||
- NSIS 安装包缺少 WebView2Loader.dll
|
- 路径验证 (红色无效、橙色重复)
|
||||||
- AppShell overflow-hidden 导致窗口无法上下滚动
|
- 环境变量路径悬浮展开预览
|
||||||
|
- 全局键盘快捷键
|
||||||
|
- 修改状态指示 + 未保存退出确认
|
||||||
|
|
||||||
### 变更
|
## 4.0.0
|
||||||
- 清理 LOW 问题:样式去重、死代码删除、命名修正
|
|
||||||
- 抽取 Modal 共享组件、统一按钮样式
|
|
||||||
- 支持 JSON/CSV/TXT 三种导入导出格式
|
|
||||||
|
|
||||||
---
|
### Added
|
||||||
|
- Tauri 2.x + React + TypeScript 首次发布
|
||||||
## [4.0.0] — 2026-05-25
|
- Windows 系统/用户 PATH 的增删改查
|
||||||
|
- 拖拽排序、多选批量删除
|
||||||
### 重大变更
|
- 实时搜索过滤
|
||||||
完全重写为 Tauri 2.x + React 19 + TypeScript + Rust 技术栈,替代原有的 C + IUP GUI。
|
- 导入导出 JSON/CSV/TXT
|
||||||
|
- 撤销/重做支持
|
||||||
### 新增
|
- 保存前自动备份注册表
|
||||||
- 现代 Web UI(React 19 + Tailwind CSS 4 + Zustand)
|
|
||||||
- 深色/浅色模式切换
|
|
||||||
- 中英文界面即时切换
|
|
||||||
- 路径有效性颜色编码(红色无效、橙色重复)
|
|
||||||
- 环境变量展开悬停提示
|
|
||||||
- 文件夹拖拽添加路径
|
|
||||||
- 保存前 PATH 长度检查
|
|
||||||
|
|
||||||
### 改进
|
|
||||||
- 完整撤销/重做支持(8 种操作类型,50 步历史)
|
|
||||||
- JSON/CSV/TXT 三种格式导入导出
|
|
||||||
- 合并预览查看系统+用户路径
|
|
||||||
- 类型安全:TypeScript strict 模式 + Rust 编译期检查
|
|
||||||
- NSIS 安装包,约 8MB
|
|
||||||
|
|
||||||
### 移除
|
|
||||||
- 旧 C + IUP + Lua + gettext 代码库
|
|
||||||
- Lua 配置引擎 → JSON 配置文件
|
|
||||||
- gettext 国际化 → i18next
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## [3.x] 及更早
|
|
||||||
|
|
||||||
C + IUP GUI 版本,已停止维护。历史发布记录见 [GitHub Releases](https://github.com/LHY0125/PathEditor/releases)。
|
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# 贡献者行为准则
|
||||||
|
|
||||||
|
## 我们的承诺
|
||||||
|
|
||||||
|
为了营造一个开放和友好的环境,我们作为贡献者和维护者承诺:无论年龄、体型、残障、种族、性别认同和表达、经验水平、国籍、个人外貌、宗教、性取向或身份,参与本项目不会受到骚扰。
|
||||||
|
|
||||||
|
## 我们的标准
|
||||||
|
|
||||||
|
有助于创造积极环境的行为包括:
|
||||||
|
|
||||||
|
- 使用友好和包容的语言
|
||||||
|
- 尊重不同的观点和经验
|
||||||
|
- 优雅地接受建设性批评
|
||||||
|
- 关注对社区最有利的事情
|
||||||
|
- 对其他社区成员表示同理心
|
||||||
|
|
||||||
|
不可接受的行为包括:
|
||||||
|
|
||||||
|
- 使用性暗示语言或图像以及不受欢迎的性关注
|
||||||
|
- 侮辱/贬损性评论以及人身攻击或政治攻击
|
||||||
|
- 公开或私下的骚扰
|
||||||
|
- 未经明确许可发布他人的私人信息
|
||||||
|
|
||||||
|
## 我们的责任
|
||||||
|
|
||||||
|
项目维护者有责任澄清可接受行为的标准,并应对任何不可接受的行为采取适当和公平的纠正措施。
|
||||||
|
|
||||||
|
## 范围
|
||||||
|
|
||||||
|
本行为准则适用于项目空间和代表项目的公共空间。
|
||||||
|
|
||||||
|
## 执行
|
||||||
|
|
||||||
|
可通过 GitHub Issues 或直接联系维护者报告辱骂、骚扰或其他不可接受的行为。所有投诉将被审查和调查,并将产生被认为必要且适合情况的回应。
|
||||||
|
|
||||||
|
本项目改编自 [Contributor Covenant](https://www.contributor-covenant.org) 2.1 版。
|
||||||
+64
-19
@@ -1,29 +1,74 @@
|
|||||||
# 贡献指南
|
# 贡献指南
|
||||||
|
|
||||||
感谢你对 PathEditor 的关注!
|
## 本地开发环境
|
||||||
|
|
||||||
## 提交 Issue
|
- **Node.js** 22+
|
||||||
|
- **Rust** 1.95+ (stable-x86_64-pc-windows-gnu)
|
||||||
|
- **MinGW-w64** (GCC 15.x 需 `-lmcfgthread` 链接标志)
|
||||||
|
- **Windows 10+** (自带 WebView2)
|
||||||
|
|
||||||
- 使用清晰的标题描述问题
|
## 开发流程
|
||||||
- 提供复现步骤
|
|
||||||
- 附上系统信息(Windows 版本、是否管理员)
|
|
||||||
- 如果是功能建议,说明使用场景
|
|
||||||
|
|
||||||
## 提交 Pull Request
|
1. Fork 本仓库
|
||||||
|
2. `git clone <你的 fork>`
|
||||||
|
3. `git checkout -b feature/xxx`
|
||||||
|
4. 开发 + 测试
|
||||||
|
5. `git commit` (遵循约定式提交格式)
|
||||||
|
6. `git push`
|
||||||
|
7. 提交 Pull Request
|
||||||
|
|
||||||
1. Fork 仓库并从 `main` 创建功能分支
|
## 运行测试
|
||||||
2. 运行 `npm test` 和 `cargo check` 确保通过
|
|
||||||
3. 遵循项目代码规范:
|
|
||||||
- TypeScript `strict: true`,零编译错误
|
|
||||||
- 前端核心逻辑在 `src/core/`,纯函数,零依赖
|
|
||||||
- Rust `unsafe` 块必须有 `// SAFETY:` 注释
|
|
||||||
4. 新功能应包含测试
|
|
||||||
|
|
||||||
## 本地开发
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
# 前端单元测试
|
||||||
npx tauri dev
|
npm test
|
||||||
|
|
||||||
|
# Rust 测试
|
||||||
|
cargo test
|
||||||
|
|
||||||
|
# E2E 测试 (需要先 npm run dev)
|
||||||
|
npx playwright test
|
||||||
|
|
||||||
|
# Clippy 检查
|
||||||
|
cargo clippy -- -D warnings
|
||||||
```
|
```
|
||||||
|
|
||||||
详见 [README.md](./README.md#开发)。
|
## 代码规范
|
||||||
|
|
||||||
|
### TypeScript
|
||||||
|
|
||||||
|
- `strict: true`,零编译错误
|
||||||
|
- 核心逻辑在 `src/core/`,纯函数,零框架依赖
|
||||||
|
- 不可变操作优先
|
||||||
|
|
||||||
|
### Rust
|
||||||
|
|
||||||
|
- 所有 `pub fn` 必须有 `///` 文档注释
|
||||||
|
- 所有 `unsafe` 块必须有 `// SAFETY:` 注释
|
||||||
|
- `cargo clippy -- -D warnings` 零警告
|
||||||
|
- `cargo fmt` 统一格式
|
||||||
|
|
||||||
|
## 提交格式
|
||||||
|
|
||||||
|
```
|
||||||
|
<类型>: <描述>
|
||||||
|
```
|
||||||
|
|
||||||
|
类型:`feat`, `fix`, `refactor`, `docs`, `test`, `chore`, `perf`, `ci`
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
core/ # Rust 核心库(零 Tauri 依赖)
|
||||||
|
gui/ # Tauri 桌面应用
|
||||||
|
cli/ # 命令行工具
|
||||||
|
src/ # React 前端
|
||||||
|
tests/unit/ # 前端单元测试
|
||||||
|
e2e/ # Playwright E2E 测试
|
||||||
|
```
|
||||||
|
|
||||||
|
## 开始贡献前
|
||||||
|
|
||||||
|
- 大改动建议先开 Issue 讨论
|
||||||
|
- 新功能需要对应的测试
|
||||||
|
- 不要引入新的 clippy 警告
|
||||||
|
|||||||
Generated
-1
@@ -2396,7 +2396,6 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"path-editor-core",
|
"path-editor-core",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-dialog",
|
"tauri-plugin-dialog",
|
||||||
|
|||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
# 安全策略
|
||||||
|
|
||||||
|
## 报告漏洞
|
||||||
|
|
||||||
|
如果你发现安全漏洞,请**不要**在公开 Issue 中报告。请通过以下方式私下报告:
|
||||||
|
|
||||||
|
- GitHub: 在 [Security Advisories](https://github.com/LHY0125/PathEditor/security/advisories) 页面提交
|
||||||
|
- 邮件: 联系项目维护者
|
||||||
|
|
||||||
|
我们会在 **48 小时内**确认收到报告,并在 7 天内提供初步评估和修复计划。
|
||||||
|
|
||||||
|
## 安全最佳实践
|
||||||
|
|
||||||
|
### 作为用户
|
||||||
|
|
||||||
|
- 仅从 [Releases](https://github.com/LHY0125/PathEditor/releases) 页面下载安装包
|
||||||
|
- 保存前确认 PATH 内容正确,备份文件存放在 `~/.patheditor/backups/`
|
||||||
|
- 编辑系统 PATH 需要管理员权限
|
||||||
|
|
||||||
|
### 作为开发者
|
||||||
|
|
||||||
|
- 永远不要在源代码中硬编码密钥或凭据
|
||||||
|
- 所有 `unsafe` 块必须有 `// SAFETY:` 注释
|
||||||
|
- 输入验证在系统边界执行
|
||||||
|
- 敏感操作(注册表写入)使用最小权限原则
|
||||||
|
|
||||||
|
## 支持版本
|
||||||
|
|
||||||
|
| 版本 | 支持状态 |
|
||||||
|
|------|----------|
|
||||||
|
| v5.x | 活跃支持 |
|
||||||
|
| v4.x | 仅安全修复 |
|
||||||
|
| < v4.0 | 不再支持 |
|
||||||
|
|
||||||
|
## 已知问题
|
||||||
|
|
||||||
|
- 备份文件格式为纯文本,不加密。如需保护备份,请将 `~/.patheditor/` 目录设为仅当前用户可读。
|
||||||
+43
-17
@@ -3,7 +3,7 @@ use path_editor_core as core;
|
|||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "patheditor", version = "5.0.0")]
|
#[command(name = "patheditor", version = env!("CARGO_PKG_VERSION"))]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: Command,
|
command: Command,
|
||||||
@@ -103,6 +103,11 @@ enum ProfileCmd {
|
|||||||
Apply { name: String },
|
Apply { name: String },
|
||||||
/// 删除配置
|
/// 删除配置
|
||||||
Delete { name: String },
|
Delete { name: String },
|
||||||
|
/// 重命名配置
|
||||||
|
Rename {
|
||||||
|
#[arg(long)] old: String,
|
||||||
|
#[arg(long)] new: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
fn exit_err(msg: &str) -> ! {
|
fn exit_err(msg: &str) -> ! {
|
||||||
@@ -146,10 +151,10 @@ fn load_and_save(system: bool, f: impl FnOnce(Vec<String>) -> Vec<String>) {
|
|||||||
fn cmd_list(system: bool, user: bool, json_out: bool) {
|
fn cmd_list(system: bool, user: bool, json_out: bool) {
|
||||||
let mut sys: Vec<String> = vec![];
|
let mut sys: Vec<String> = vec![];
|
||||||
let mut usr: Vec<String> = vec![];
|
let mut usr: Vec<String> = vec![];
|
||||||
if system || (!system && !user) {
|
if system || !user {
|
||||||
sys = core::registry::load_system_paths().unwrap_or_else(|e| exit_err(&e));
|
sys = core::registry::load_system_paths().unwrap_or_else(|e| exit_err(&e));
|
||||||
}
|
}
|
||||||
if user || (!system && !user) {
|
if user || !system {
|
||||||
usr = core::registry::load_user_paths().unwrap_or_else(|e| exit_err(&e));
|
usr = core::registry::load_user_paths().unwrap_or_else(|e| exit_err(&e));
|
||||||
}
|
}
|
||||||
if json_out {
|
if json_out {
|
||||||
@@ -169,7 +174,7 @@ fn cmd_list(system: bool, user: bool, json_out: bool) {
|
|||||||
|
|
||||||
fn cmd_add(path: String, system: bool, user: bool) {
|
fn cmd_add(path: String, system: bool, user: bool) {
|
||||||
let target = ensure_single_target(system, user);
|
let target = ensure_single_target(system, user);
|
||||||
load_and_save(system || false, |mut list| {
|
load_and_save(system, |mut list| {
|
||||||
list.push(path.clone());
|
list.push(path.clone());
|
||||||
list
|
list
|
||||||
});
|
});
|
||||||
@@ -209,10 +214,10 @@ fn cmd_edit(index: usize, new_path: String, system: bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn cmd_move(index: usize, steps: usize, system: bool, up: bool) {
|
fn cmd_move(index: usize, steps: usize, system: bool, up: bool) {
|
||||||
load_and_save(system || false, |mut list| {
|
load_and_save(system, |mut list| {
|
||||||
if index >= list.len() { exit_err(&format!("索引 {index} 超出范围 (共 {} 条)", list.len())); }
|
if index >= list.len() { exit_err(&format!("索引 {index} 超出范围 (共 {} 条)", list.len())); }
|
||||||
let end = if up {
|
let end = if up {
|
||||||
if steps > index { 0 } else { index - steps }
|
index.saturating_sub(steps)
|
||||||
} else {
|
} else {
|
||||||
let max = list.len() - 1;
|
let max = list.len() - 1;
|
||||||
if index + steps > max { max } else { index + steps }
|
if index + steps > max { max } else { index + steps }
|
||||||
@@ -227,7 +232,19 @@ fn cmd_move(index: usize, steps: usize, system: bool, up: bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn cmd_clean(system: bool, user: bool, dry_run: bool, json_out: bool) {
|
fn cmd_clean(system: bool, user: bool, dry_run: bool, json_out: bool) {
|
||||||
let target = ensure_single_target(system, user);
|
if system && user { exit_err("不能同时指定 --system 和 --user"); }
|
||||||
|
|
||||||
|
let clean_sys = system || !user;
|
||||||
|
let clean_usr = user || !system;
|
||||||
|
|
||||||
|
if clean_sys { clean_one("system", dry_run, json_out); }
|
||||||
|
if clean_usr { clean_one("user", dry_run, json_out); }
|
||||||
|
|
||||||
|
if !dry_run && !json_out { core::system::broadcast_env_change(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clean_one(target: &str, dry_run: bool, json_out: bool) {
|
||||||
|
let label = if target == "system" { "系统" } else { "用户" };
|
||||||
let list = if target == "system" {
|
let list = if target == "system" {
|
||||||
core::registry::load_system_paths().unwrap_or_else(|e| exit_err(&e))
|
core::registry::load_system_paths().unwrap_or_else(|e| exit_err(&e))
|
||||||
} else {
|
} else {
|
||||||
@@ -236,17 +253,16 @@ fn cmd_clean(system: bool, user: bool, dry_run: bool, json_out: bool) {
|
|||||||
let (kept, removed) = core::registry::clean_paths(list.clone());
|
let (kept, removed) = core::registry::clean_paths(list.clone());
|
||||||
|
|
||||||
if json_out {
|
if json_out {
|
||||||
println!("{}", json!({ "kept": kept, "removed": removed, "kept_count": kept.len(), "removed_count": removed.len() }).to_string());
|
println!("{}", json!({ "target": target, "kept": kept, "removed": removed, "kept_count": kept.len(), "removed_count": removed.len() }));
|
||||||
} else if dry_run {
|
} else if dry_run {
|
||||||
println!("═══ 将被移除({} 条)═══", removed.len());
|
println!("═══ {label} PATH — 将被移除({} 条)═══", removed.len());
|
||||||
for r in &removed { println!(" ✗ {}", r); }
|
for r in &removed { println!(" ✗ {}", r); }
|
||||||
println!("═══ 将保留({} 条)═══", kept.len());
|
println!("═══ {label} PATH — 将保留({} 条)═══", kept.len());
|
||||||
for k in &kept { println!(" ✓ {}", k); }
|
for k in &kept { println!(" ✓ {}", k); }
|
||||||
} else {
|
} else {
|
||||||
let kept_count = kept.len();
|
let kept_count = kept.len();
|
||||||
verify_and_save(target, &list, kept);
|
verify_and_save(target, &list, kept);
|
||||||
println!("清理完成:移除 {} 条,保留 {} 条", removed.len(), kept_count);
|
println!("{label} PATH 清理完成:移除 {} 条,保留 {} 条", removed.len(), kept_count);
|
||||||
core::system::broadcast_env_change();
|
|
||||||
if !removed.is_empty() {
|
if !removed.is_empty() {
|
||||||
for r in &removed { println!(" 已移除: {}", r); }
|
for r in &removed { println!(" 已移除: {}", r); }
|
||||||
}
|
}
|
||||||
@@ -304,7 +320,7 @@ fn cmd_import(file: String, target: String) {
|
|||||||
fn cmd_export(format: String, output: Option<String>) {
|
fn cmd_export(format: String, output: Option<String>) {
|
||||||
let sys = core::registry::load_system_paths().unwrap_or_else(|e| exit_err(&e));
|
let sys = core::registry::load_system_paths().unwrap_or_else(|e| exit_err(&e));
|
||||||
let usr = core::registry::load_user_paths().unwrap_or_else(|e| exit_err(&e));
|
let usr = core::registry::load_user_paths().unwrap_or_else(|e| exit_err(&e));
|
||||||
let content = core::fs::export_paths(&sys, &usr, &format);
|
let content = core::fs::export_paths(&sys, &usr, &format).unwrap_or_else(|e| exit_err(&e));
|
||||||
if let Some(path) = output {
|
if let Some(path) = output {
|
||||||
std::fs::write(&path, &content).unwrap_or_else(|e| exit_err(&format!("无法写入文件: {e}")));
|
std::fs::write(&path, &content).unwrap_or_else(|e| exit_err(&format!("无法写入文件: {e}")));
|
||||||
println!("已导出到: {path}");
|
println!("已导出到: {path}");
|
||||||
@@ -394,10 +410,14 @@ fn profile_load(name: String) {
|
|||||||
|
|
||||||
fn profile_apply(name: String) {
|
fn profile_apply(name: String) {
|
||||||
let data = core::profiles::load_profile(&name).unwrap_or_else(|e| exit_err(&e));
|
let data = core::profiles::load_profile(&name).unwrap_or_else(|e| exit_err(&e));
|
||||||
let sys: Vec<String> = data.sys.into_iter().filter(|e| e.enabled).map(|e| e.path).collect();
|
let new_sys: Vec<String> = data.sys.into_iter().filter(|e| e.enabled).map(|e| e.path).collect();
|
||||||
let usr: Vec<String> = data.user.into_iter().filter(|e| e.enabled).map(|e| e.path).collect();
|
let new_usr: Vec<String> = data.user.into_iter().filter(|e| e.enabled).map(|e| e.path).collect();
|
||||||
core::registry::save_system_paths(sys).unwrap_or_else(|e| exit_err(&e));
|
|
||||||
core::registry::save_user_paths(usr).unwrap_or_else(|e| exit_err(&e));
|
let orig_sys = core::registry::load_system_paths().unwrap_or_else(|e| exit_err(&e));
|
||||||
|
let orig_usr = core::registry::load_user_paths().unwrap_or_else(|e| exit_err(&e));
|
||||||
|
verify_and_save("system", &orig_sys, new_sys);
|
||||||
|
verify_and_save("user", &orig_usr, new_usr);
|
||||||
|
|
||||||
core::system::broadcast_env_change();
|
core::system::broadcast_env_change();
|
||||||
println!("配置文件 \"{name}\" 已写入注册表。");
|
println!("配置文件 \"{name}\" 已写入注册表。");
|
||||||
}
|
}
|
||||||
@@ -407,6 +427,11 @@ fn profile_delete(name: String) {
|
|||||||
println!("已删除配置: {name}");
|
println!("已删除配置: {name}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn profile_rename(old_name: String, new_name: String) {
|
||||||
|
core::profiles::rename_profile(&old_name, &new_name).unwrap_or_else(|e| exit_err(&e));
|
||||||
|
println!("已重命名: {old_name} → {new_name}");
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
match cli.command {
|
match cli.command {
|
||||||
@@ -431,6 +456,7 @@ fn main() {
|
|||||||
ProfileCmd::Load { name } => profile_load(name),
|
ProfileCmd::Load { name } => profile_load(name),
|
||||||
ProfileCmd::Apply { name } => profile_apply(name),
|
ProfileCmd::Apply { name } => profile_apply(name),
|
||||||
ProfileCmd::Delete { name } => profile_delete(name),
|
ProfileCmd::Delete { name } => profile_delete(name),
|
||||||
|
ProfileCmd::Rename { old, new } => profile_rename(old, new),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-5
@@ -4,21 +4,19 @@ use winreg::enums::*;
|
|||||||
use crate::registry::{self, SYS_REG_PATH, USER_REG_PATH};
|
use crate::registry::{self, SYS_REG_PATH, USER_REG_PATH};
|
||||||
|
|
||||||
fn backup_base_dir() -> PathBuf {
|
fn backup_base_dir() -> PathBuf {
|
||||||
dirs::data_dir()
|
dirs::home_dir()
|
||||||
.or_else(dirs::home_dir)
|
|
||||||
.unwrap_or_else(|| PathBuf::from("."))
|
.unwrap_or_else(|| PathBuf::from("."))
|
||||||
.join("PathEditor")
|
.join(".patheditor")
|
||||||
.join("backups")
|
.join("backups")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取 APPDATA 路径下的备份目录
|
/// 获取备份目录路径
|
||||||
pub fn get_appdata_dir() -> String {
|
pub fn get_appdata_dir() -> String {
|
||||||
backup_base_dir().to_string_lossy().to_string()
|
backup_base_dir().to_string_lossy().to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 备份当前注册表中的系统 PATH 和用户 PATH
|
/// 备份当前注册表中的系统 PATH 和用户 PATH
|
||||||
/// 在保存前调用,备份的是注册表中的当前值(保存前的状态)
|
/// 在保存前调用,备份的是注册表中的当前值(保存前的状态)
|
||||||
|
|
||||||
pub fn backup_registry(custom_dir: Option<String>) -> Result<String, String> {
|
pub fn backup_registry(custom_dir: Option<String>) -> Result<String, String> {
|
||||||
let backup_dir = match custom_dir {
|
let backup_dir = match custom_dir {
|
||||||
Some(ref dir) if !dir.is_empty() => std::path::PathBuf::from(dir),
|
Some(ref dir) if !dir.is_empty() => std::path::PathBuf::from(dir),
|
||||||
@@ -65,3 +63,33 @@ pub fn backup_registry(custom_dir: Option<String>) -> Result<String, String> {
|
|||||||
log::info!("备份已保存到: {}", result);
|
log::info!("备份已保存到: {}", result);
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_appdata_dir_returns_non_empty() {
|
||||||
|
assert!(!get_appdata_dir().is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn backup_registry_with_custom_dir() {
|
||||||
|
let dir = std::env::temp_dir().join("patheditor_test_backup_custom");
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
|
|
||||||
|
let result = backup_registry(Some(dir.to_string_lossy().to_string()));
|
||||||
|
// 可能因无权限读取注册表而失败,但不应 panic
|
||||||
|
if let Ok(path) = result {
|
||||||
|
assert!(path.contains("patheditor_test_backup_custom"));
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn backup_registry_default_dir_no_panic() {
|
||||||
|
// 验证不传参时不会 panic
|
||||||
|
let _ = backup_registry(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+29
-3
@@ -3,10 +3,9 @@ use std::fs;
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
fn disabled_file_path() -> PathBuf {
|
fn disabled_file_path() -> PathBuf {
|
||||||
dirs::data_dir()
|
dirs::home_dir()
|
||||||
.or_else(dirs::home_dir)
|
|
||||||
.unwrap_or_else(|| PathBuf::from("."))
|
.unwrap_or_else(|| PathBuf::from("."))
|
||||||
.join("PathEditor")
|
.join(".patheditor")
|
||||||
.join("disabled.json")
|
.join("disabled.json")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,3 +57,30 @@ pub fn load_disabled_state() -> Result<(Vec<String>, Vec<String>), String> {
|
|||||||
|
|
||||||
Ok((state.system, state.user))
|
Ok((state.system, state.user))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn disabled_state() {
|
||||||
|
// roundtrip
|
||||||
|
let sys = vec!["C:\\sys1".into(), "C:\\sys2".into()];
|
||||||
|
let usr = vec!["D:\\usr1".into()];
|
||||||
|
save_disabled_state(sys.clone(), usr.clone()).unwrap();
|
||||||
|
let (loaded_sys, loaded_usr) = load_disabled_state().unwrap();
|
||||||
|
assert_eq!(loaded_sys, sys);
|
||||||
|
assert_eq!(loaded_usr, usr);
|
||||||
|
|
||||||
|
// overwrite
|
||||||
|
let new_sys = vec!["C:\\new".into()];
|
||||||
|
save_disabled_state(new_sys.clone(), vec![]).unwrap();
|
||||||
|
let (loaded, _) = load_disabled_state().unwrap();
|
||||||
|
assert_eq!(loaded, new_sys);
|
||||||
|
|
||||||
|
// empty
|
||||||
|
save_disabled_state(vec![], vec![]).unwrap();
|
||||||
|
let result = load_disabled_state().unwrap();
|
||||||
|
assert!(result.0.is_empty() && result.1.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+125
-7
@@ -1,3 +1,6 @@
|
|||||||
|
// 注意:TS 端 src/core/import-export.ts 有对应的导入导出实现,
|
||||||
|
// 前端使用 TS 版(需 ImportDialog 交互),CLI 使用 Rust 版,修改时需同步两端。
|
||||||
|
|
||||||
/// 读取文本文件内容(供前端原生对话框选择文件后使用)
|
/// 读取文本文件内容(供前端原生对话框选择文件后使用)
|
||||||
pub fn read_text_file(path: &str) -> Result<String, String> {
|
pub fn read_text_file(path: &str) -> Result<String, String> {
|
||||||
std::fs::read_to_string(path).map_err(|e| format!("无法读取文件: {}", e))
|
std::fs::read_to_string(path).map_err(|e| format!("无法读取文件: {}", e))
|
||||||
@@ -35,9 +38,26 @@ fn import_json(content: &str) -> Result<(Vec<String>, Vec<String>), String> {
|
|||||||
fn import_csv(content: &str) -> Result<(Vec<String>, Vec<String>), String> {
|
fn import_csv(content: &str) -> Result<(Vec<String>, Vec<String>), String> {
|
||||||
let mut sys = Vec::new();
|
let mut sys = Vec::new();
|
||||||
let mut usr = Vec::new();
|
let mut usr = Vec::new();
|
||||||
|
let mut first = true;
|
||||||
for line in content.lines() {
|
for line in content.lines() {
|
||||||
let trimmed = line.trim();
|
let mut trimmed = line.trim();
|
||||||
if trimmed.is_empty() { continue; }
|
if trimmed.is_empty() { continue; }
|
||||||
|
|
||||||
|
// 处理 UTF-8 BOM(仅首行)
|
||||||
|
if first {
|
||||||
|
first = false;
|
||||||
|
if let Some(stripped) = trimmed.strip_prefix('\u{FEFF}') {
|
||||||
|
trimmed = stripped;
|
||||||
|
}
|
||||||
|
// 跳过 header 行 "type,path"
|
||||||
|
let fields: Vec<&str> = trimmed.split(',').collect();
|
||||||
|
if fields.len() >= 2 {
|
||||||
|
let c0 = fields[0].trim().to_lowercase();
|
||||||
|
let c1 = fields[1].trim().to_lowercase();
|
||||||
|
if c0 == "type" && c1 == "path" { continue; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let fields: Vec<&str> = trimmed.split(',').collect();
|
let fields: Vec<&str> = trimmed.split(',').collect();
|
||||||
if fields.len() >= 2 {
|
if fields.len() >= 2 {
|
||||||
match fields[0].trim().to_lowercase().as_str() {
|
match fields[0].trim().to_lowercase().as_str() {
|
||||||
@@ -69,16 +89,16 @@ fn import_txt(content: &str) -> Result<(Vec<String>, Vec<String>), String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 导出 PATH 为指定格式字符串
|
/// 导出 PATH 为指定格式字符串
|
||||||
pub fn export_paths(sys: &[String], usr: &[String], format: &str) -> String {
|
pub fn export_paths(sys: &[String], usr: &[String], format: &str) -> Result<String, String> {
|
||||||
match format {
|
match format {
|
||||||
"json" => {
|
"json" => {
|
||||||
let data = serde_json::json!({
|
let data = serde_json::json!({
|
||||||
"version": "5.0.0",
|
"version": env!("CARGO_PKG_VERSION"),
|
||||||
"timestamp": chrono::Local::now().format("%Y-%m-%dT%H:%M:%S").to_string(),
|
"timestamp": chrono::Local::now().format("%Y-%m-%dT%H:%M:%S").to_string(),
|
||||||
"system": sys,
|
"system": sys,
|
||||||
"user": usr,
|
"user": usr,
|
||||||
});
|
});
|
||||||
serde_json::to_string_pretty(&data).unwrap_or_default()
|
Ok(serde_json::to_string_pretty(&data).unwrap_or_default())
|
||||||
}
|
}
|
||||||
"csv" => {
|
"csv" => {
|
||||||
let mut out = String::from("type,path\n");
|
let mut out = String::from("type,path\n");
|
||||||
@@ -88,9 +108,9 @@ pub fn export_paths(sys: &[String], usr: &[String], format: &str) -> String {
|
|||||||
for p in usr {
|
for p in usr {
|
||||||
out.push_str(&format!("user,{}\n", p));
|
out.push_str(&format!("user,{}\n", p));
|
||||||
}
|
}
|
||||||
out
|
Ok(out)
|
||||||
}
|
}
|
||||||
_ => {
|
"txt" => {
|
||||||
let mut out = String::new();
|
let mut out = String::new();
|
||||||
if !sys.is_empty() {
|
if !sys.is_empty() {
|
||||||
out.push_str(&format!("# 系统 PATH ({})\n", sys.len()));
|
out.push_str(&format!("# 系统 PATH ({})\n", sys.len()));
|
||||||
@@ -104,7 +124,105 @@ pub fn export_paths(sys: &[String], usr: &[String], format: &str) -> String {
|
|||||||
out.push_str(&format!("{}\n", p));
|
out.push_str(&format!("{}\n", p));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
_ => Err(format!("不支持的导出格式: {}", format)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn import_json_valid() {
|
||||||
|
let json = r#"{"system": ["C:\\sys1", "C:\\sys2"], "user": ["D:\\usr1"]}"#;
|
||||||
|
let (sys, usr) = import_json(json).unwrap();
|
||||||
|
assert_eq!(sys, vec!["C:\\sys1", "C:\\sys2"]);
|
||||||
|
assert_eq!(usr, vec!["D:\\usr1"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn import_json_empty_arrays() {
|
||||||
|
let (sys, usr) = import_json(r#"{"system": [], "user": []}"#).unwrap();
|
||||||
|
assert!(sys.is_empty() && usr.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn import_json_missing_fields() {
|
||||||
|
let (sys, usr) = import_json(r#"{}"#).unwrap();
|
||||||
|
assert!(sys.is_empty() && usr.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn import_csv_valid() {
|
||||||
|
let csv = "type,path\nsystem,C:\\sys1\nuser,D:\\usr1\n";
|
||||||
|
let (sys, _usr) = import_csv(csv).unwrap();
|
||||||
|
assert_eq!(sys, vec!["C:\\sys1"]);
|
||||||
|
assert_eq!(_usr, vec!["D:\\usr1"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn import_csv_with_bom() {
|
||||||
|
let csv = "\u{FEFF}type,path\nsystem,C:\\sys1\n";
|
||||||
|
let (sys, _) = import_csv(csv).unwrap();
|
||||||
|
assert_eq!(sys, vec!["C:\\sys1"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn import_csv_empty() {
|
||||||
|
assert!(import_csv("type,path\n").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn import_csv_alternate_type_names() {
|
||||||
|
let csv = "type,path\nsys,D:\\a\nusr,D:\\b\n";
|
||||||
|
let (sys, usr) = import_csv(csv).unwrap();
|
||||||
|
assert_eq!(sys, vec!["D:\\a"]);
|
||||||
|
assert_eq!(usr, vec!["D:\\b"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn export_json_roundtrip() {
|
||||||
|
let sys = vec!["C:\\a".into()];
|
||||||
|
let usr: Vec<String> = vec![];
|
||||||
|
let exported = export_paths(&sys, &usr, "json").unwrap();
|
||||||
|
let parsed: serde_json::Value = serde_json::from_str(&exported).unwrap();
|
||||||
|
assert_eq!(parsed["system"][0], "C:\\a");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn export_csv_roundtrip() {
|
||||||
|
let sys = vec!["C:\\a".into()];
|
||||||
|
let usr = vec!["D:\\b".into()];
|
||||||
|
let exported = export_paths(&sys, &usr, "csv").unwrap();
|
||||||
|
assert!(exported.contains("system,C:\\a"));
|
||||||
|
assert!(exported.contains("user,D:\\b"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn export_txt_roundtrip() {
|
||||||
|
let sys = vec!["C:\\a".into()];
|
||||||
|
let usr = vec!["D:\\b".into()];
|
||||||
|
let exported = export_paths(&sys, &usr, "txt").unwrap();
|
||||||
|
assert!(exported.contains("C:\\a") && exported.contains("D:\\b"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn export_invalid_format_errors() {
|
||||||
|
assert!(export_paths(&[], &[], "xml").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn import_paths_detects_format() {
|
||||||
|
let (sys, _) = import_paths("test.csv", "type,path\nsystem,C:\\x\n").unwrap();
|
||||||
|
assert_eq!(sys, vec!["C:\\x"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn import_paths_txt_to_user() {
|
||||||
|
let (sys, usr) = import_paths("test.txt", "C:\\x\nD:\\y\n").unwrap();
|
||||||
|
assert!(sys.is_empty());
|
||||||
|
assert_eq!(usr, vec!["C:\\x", "D:\\y"]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+89
-8
@@ -9,6 +9,19 @@ fn profiles_dir() -> PathBuf {
|
|||||||
.join("profiles")
|
.join("profiles")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn validate_profile_name(name: &str) -> Result<(), String> {
|
||||||
|
if name.is_empty() { return Err("配置名称不能为空".into()); }
|
||||||
|
if name.contains('/') || name.contains('\\') || name.contains("..") {
|
||||||
|
return Err("配置名称包含非法字符".into());
|
||||||
|
}
|
||||||
|
for ch in name.chars() {
|
||||||
|
if "<>:\"|?*".contains(ch) {
|
||||||
|
return Err("配置名称包含非法字符".into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn profile_path(name: &str) -> PathBuf {
|
fn profile_path(name: &str) -> PathBuf {
|
||||||
profiles_dir().join(format!("{}.json", name))
|
profiles_dir().join(format!("{}.json", name))
|
||||||
}
|
}
|
||||||
@@ -48,11 +61,13 @@ pub fn list_profiles() -> Result<Vec<ProfileMeta>, String> {
|
|||||||
|
|
||||||
for entry in entries.flatten() {
|
for entry in entries.flatten() {
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
if path.extension().map_or(true, |e| e != "json") {
|
if path.extension().is_none_or(|e| e != "json") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let content = fs::read_to_string(&path)
|
let content = match fs::read_to_string(&path) {
|
||||||
.map_err(|e| format!("无法读取 {}: {}", path.display(), e))?;
|
Ok(c) => c,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
if let Ok(data) = serde_json::from_str::<ProfileData>(&content) {
|
if let Ok(data) = serde_json::from_str::<ProfileData>(&content) {
|
||||||
profiles.push(ProfileMeta {
|
profiles.push(ProfileMeta {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
@@ -72,10 +87,11 @@ pub fn save_profile(
|
|||||||
sys: Vec<ProfilePathEntry>,
|
sys: Vec<ProfilePathEntry>,
|
||||||
user: Vec<ProfilePathEntry>,
|
user: Vec<ProfilePathEntry>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
|
validate_profile_name(name)?;
|
||||||
let dir = profiles_dir();
|
let dir = profiles_dir();
|
||||||
fs::create_dir_all(&dir).map_err(|e| format!("无法创建配置目录: {}", e))?;
|
fs::create_dir_all(&dir).map_err(|e| format!("无法创建配置目录: {}", e))?;
|
||||||
|
|
||||||
let path = profile_path(&name);
|
let path = profile_path(name);
|
||||||
let now = chrono::Local::now().format("%Y-%m-%dT%H:%M:%S").to_string();
|
let now = chrono::Local::now().format("%Y-%m-%dT%H:%M:%S").to_string();
|
||||||
|
|
||||||
// 覆盖已有配置时保留原始创建时间
|
// 覆盖已有配置时保留原始创建时间
|
||||||
@@ -107,7 +123,8 @@ pub fn save_profile(
|
|||||||
|
|
||||||
/// 加载配置文件
|
/// 加载配置文件
|
||||||
pub fn load_profile(name: &str) -> Result<ProfileData, String> {
|
pub fn load_profile(name: &str) -> Result<ProfileData, String> {
|
||||||
let path = profile_path(&name);
|
validate_profile_name(name)?;
|
||||||
|
let path = profile_path(name);
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
return Err(format!("配置文件不存在: {}", name));
|
return Err(format!("配置文件不存在: {}", name));
|
||||||
}
|
}
|
||||||
@@ -119,7 +136,8 @@ pub fn load_profile(name: &str) -> Result<ProfileData, String> {
|
|||||||
|
|
||||||
/// 删除配置文件
|
/// 删除配置文件
|
||||||
pub fn delete_profile(name: &str) -> Result<(), String> {
|
pub fn delete_profile(name: &str) -> Result<(), String> {
|
||||||
let path = profile_path(&name);
|
validate_profile_name(name)?;
|
||||||
|
let path = profile_path(name);
|
||||||
fs::remove_file(&path).map_err(|e| format!("无法删除配置文件: {}", e))?;
|
fs::remove_file(&path).map_err(|e| format!("无法删除配置文件: {}", e))?;
|
||||||
log::info!("已删除配置: {}", path.display());
|
log::info!("已删除配置: {}", path.display());
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -127,7 +145,9 @@ pub fn delete_profile(name: &str) -> Result<(), String> {
|
|||||||
|
|
||||||
/// 重命名配置文件
|
/// 重命名配置文件
|
||||||
pub fn rename_profile(old_name: &str, new_name: &str) -> Result<(), String> {
|
pub fn rename_profile(old_name: &str, new_name: &str) -> Result<(), String> {
|
||||||
let old_path = profile_path(&old_name);
|
validate_profile_name(old_name)?;
|
||||||
|
validate_profile_name(new_name)?;
|
||||||
|
let old_path = profile_path(old_name);
|
||||||
if !old_path.exists() {
|
if !old_path.exists() {
|
||||||
return Err(format!("配置文件不存在: {}", old_name));
|
return Err(format!("配置文件不存在: {}", old_name));
|
||||||
}
|
}
|
||||||
@@ -138,7 +158,7 @@ pub fn rename_profile(old_name: &str, new_name: &str) -> Result<(), String> {
|
|||||||
data.name = new_name.to_string();
|
data.name = new_name.to_string();
|
||||||
data.modified = chrono::Local::now().format("%Y-%m-%dT%H:%M:%S").to_string();
|
data.modified = chrono::Local::now().format("%Y-%m-%dT%H:%M:%S").to_string();
|
||||||
|
|
||||||
let new_path = profile_path(&new_name);
|
let new_path = profile_path(new_name);
|
||||||
let json =
|
let json =
|
||||||
serde_json::to_string_pretty(&data).map_err(|e| format!("JSON 序列化失败: {}", e))?;
|
serde_json::to_string_pretty(&data).map_err(|e| format!("JSON 序列化失败: {}", e))?;
|
||||||
fs::write(&new_path, &json).map_err(|e| format!("无法写入配置文件: {}", e))?;
|
fs::write(&new_path, &json).map_err(|e| format!("无法写入配置文件: {}", e))?;
|
||||||
@@ -150,3 +170,64 @@ pub fn rename_profile(old_name: &str, new_name: &str) -> Result<(), String> {
|
|||||||
log::info!("已重命名配置: {} -> {}", old_name, new_name);
|
log::info!("已重命名配置: {} -> {}", old_name, new_name);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn test_entry(path: &str) -> ProfilePathEntry {
|
||||||
|
ProfilePathEntry { path: path.into(), enabled: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validate_name_rejects_empty() {
|
||||||
|
assert!(validate_profile_name("").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validate_name_rejects_path_traversal() {
|
||||||
|
assert!(validate_profile_name("../../evil").is_err());
|
||||||
|
assert!(validate_profile_name("foo\\bar").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validate_name_rejects_reserved_chars() {
|
||||||
|
assert!(validate_profile_name("foo:bar").is_err());
|
||||||
|
assert!(validate_profile_name("foo<bar").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn profile_crud() {
|
||||||
|
// save -> load -> delete
|
||||||
|
let name = "__test_profile_crud";
|
||||||
|
let _ = delete_profile(name);
|
||||||
|
save_profile(name, vec![test_entry("C:\\sys")], vec![test_entry("D:\\usr")]).unwrap();
|
||||||
|
let loaded = load_profile(name).unwrap();
|
||||||
|
assert_eq!(loaded.sys[0].path, "C:\\sys");
|
||||||
|
delete_profile(name).unwrap();
|
||||||
|
assert!(load_profile(name).is_err());
|
||||||
|
|
||||||
|
// rename
|
||||||
|
let old_name = "__test_rename_old";
|
||||||
|
let new_name = "__test_rename_new";
|
||||||
|
let _ = delete_profile(old_name);
|
||||||
|
let _ = delete_profile(new_name);
|
||||||
|
save_profile(old_name, vec![test_entry("C:\\x")], vec![]).unwrap();
|
||||||
|
rename_profile(old_name, new_name).unwrap();
|
||||||
|
assert!(load_profile(old_name).is_err());
|
||||||
|
let renamed = load_profile(new_name).unwrap();
|
||||||
|
assert_eq!(renamed.name, new_name);
|
||||||
|
delete_profile(new_name).unwrap();
|
||||||
|
|
||||||
|
// list
|
||||||
|
let _ = delete_profile("__test_list_a");
|
||||||
|
let _ = delete_profile("__test_list_b");
|
||||||
|
save_profile("__test_list_a", vec![], vec![]).unwrap();
|
||||||
|
save_profile("__test_list_b", vec![], vec![]).unwrap();
|
||||||
|
let list = list_profiles().unwrap();
|
||||||
|
let names: Vec<&str> = list.iter().map(|m| m.name.as_str()).collect();
|
||||||
|
assert!(names.contains(&"__test_list_a"));
|
||||||
|
delete_profile("__test_list_a").unwrap();
|
||||||
|
delete_profile("__test_list_b").unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -44,21 +44,41 @@ fn save_paths(root: winreg::HKEY, sub_path: &str, label: &str, paths: &[String])
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// 从 HKLM 注册表读取系统 PATH
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// - `Ok(Vec<String>)` — 系统 PATH 路径列表
|
||||||
|
/// - `Err(String)` — 注册表读取失败
|
||||||
pub fn load_system_paths() -> Result<Vec<String>, String> {
|
pub fn load_system_paths() -> Result<Vec<String>, String> {
|
||||||
load_paths(HKEY_LOCAL_MACHINE, SYS_REG_PATH, "系统")
|
load_paths(HKEY_LOCAL_MACHINE, SYS_REG_PATH, "系统")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// 从 HKCU 注册表读取用户 PATH
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// - `Ok(Vec<String>)` — 用户 PATH 路径列表
|
||||||
|
/// - `Err(String)` — 注册表读取失败
|
||||||
pub fn load_user_paths() -> Result<Vec<String>, String> {
|
pub fn load_user_paths() -> Result<Vec<String>, String> {
|
||||||
load_paths(HKEY_CURRENT_USER, USER_REG_PATH, "用户")
|
load_paths(HKEY_CURRENT_USER, USER_REG_PATH, "用户")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// 保存系统 PATH 到注册表,含 32767 字符上限检查
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// - `Ok(())` — 保存成功
|
||||||
|
/// - `Err(String)` — 写入失败或超过字符上限
|
||||||
pub fn save_system_paths(paths: Vec<String>) -> Result<(), String> {
|
pub fn save_system_paths(paths: Vec<String>) -> Result<(), String> {
|
||||||
save_paths(HKEY_LOCAL_MACHINE, SYS_REG_PATH, "系统", &paths)
|
save_paths(HKEY_LOCAL_MACHINE, SYS_REG_PATH, "系统", &paths)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// 保存用户 PATH 到注册表
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// - `Ok(())` — 保存成功
|
||||||
|
/// - `Err(String)` — 写入失败
|
||||||
pub fn save_user_paths(paths: Vec<String>) -> Result<(), String> {
|
pub fn save_user_paths(paths: Vec<String>) -> Result<(), String> {
|
||||||
save_paths(HKEY_CURRENT_USER, USER_REG_PATH, "用户", &paths)
|
save_paths(HKEY_CURRENT_USER, USER_REG_PATH, "用户", &paths)
|
||||||
}
|
}
|
||||||
|
|||||||
+110
-42
@@ -23,33 +23,50 @@ pub struct ToolGroup {
|
|||||||
pub exes: Vec<String>,
|
pub exes: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 扫描 PATH 中的可执行文件冲突
|
/// 扫描单个目录中的可执行文件名
|
||||||
///
|
fn list_exes(dir: &str) -> Vec<String> {
|
||||||
/// 遍历每个 PATH 目录,查找 .exe/.bat/.cmd/.com/.ps1 文件,
|
|
||||||
/// 标记出现在多个目录中的同名文件(后面的目录会被前面的「遮蔽」)
|
|
||||||
|
|
||||||
pub fn scan_conflicts(paths: Vec<String>) -> Result<Vec<ConflictEntry>, String> {
|
|
||||||
// exe_name (小写) → [(priority, dir)]
|
|
||||||
let mut map: HashMap<String, Vec<(usize, String)>> = HashMap::new();
|
|
||||||
|
|
||||||
for (priority, dir) in paths.iter().enumerate() {
|
|
||||||
let p = Path::new(dir);
|
let p = Path::new(dir);
|
||||||
if !p.is_dir() {
|
if !p.is_dir() {
|
||||||
continue;
|
return vec![];
|
||||||
}
|
}
|
||||||
let entries = fs::read_dir(p).map_err(|e| format!("无法读取目录 {}: {}", dir, e))?;
|
let mut exes: Vec<String> = Vec::new();
|
||||||
|
if let Ok(entries) = fs::read_dir(p) {
|
||||||
for entry in entries.flatten() {
|
for entry in entries.flatten() {
|
||||||
let fname = entry.file_name();
|
let fname = entry.file_name();
|
||||||
let name = fname.to_string_lossy();
|
let name = fname.to_string_lossy();
|
||||||
if let Some(ext) = Path::new(name.as_ref()).extension() {
|
if let Some(ext) = Path::new(name.as_ref()).extension() {
|
||||||
let ext_lower = ext.to_ascii_lowercase();
|
let ext_lower = ext.to_ascii_lowercase();
|
||||||
if EXECUTABLE_EXTENSIONS.contains(&ext_lower.to_str().unwrap_or("")) {
|
if EXECUTABLE_EXTENSIONS.contains(&ext_lower.to_str().unwrap_or("")) {
|
||||||
let key = name.to_lowercase();
|
exes.push(name.to_string());
|
||||||
map.entry(key).or_default().push((priority, dir.clone()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
exes
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 扫描 PATH 中的可执行文件冲突
|
||||||
|
///
|
||||||
|
/// 并行遍历每个 PATH 目录,查找 .exe/.bat/.cmd/.com/.ps1 文件,
|
||||||
|
/// 标记出现在多个目录中的同名文件(后面的目录会被前面的「遮蔽」)
|
||||||
|
pub fn scan_conflicts(paths: Vec<String>) -> Result<Vec<ConflictEntry>, String> {
|
||||||
|
// 并行扫描各目录
|
||||||
|
let results: Vec<(usize, String, Vec<String>)> = std::thread::scope(|s| {
|
||||||
|
let handles: Vec<_> = paths.iter().enumerate().map(|(priority, dir)| {
|
||||||
|
s.spawn(move || (priority, dir.clone(), list_exes(dir)))
|
||||||
|
}).collect();
|
||||||
|
handles.into_iter().map(|h| h.join().unwrap()).collect()
|
||||||
|
});
|
||||||
|
|
||||||
|
// 合并: exe_name (小写) → [(priority, dir)]
|
||||||
|
let mut map: HashMap<String, Vec<(usize, String)>> = HashMap::new();
|
||||||
|
for (priority, dir, exes) in results {
|
||||||
|
for name in exes {
|
||||||
|
map.entry(name.to_lowercase())
|
||||||
|
.or_default()
|
||||||
|
.push((priority, dir.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut results: Vec<ConflictEntry> = map
|
let mut results: Vec<ConflictEntry> = map
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -69,45 +86,96 @@ pub fn scan_conflicts(paths: Vec<String>) -> Result<Vec<ConflictEntry>, String>
|
|||||||
|
|
||||||
/// 扫描 PATH 中各目录提供的可执行文件
|
/// 扫描 PATH 中各目录提供的可执行文件
|
||||||
///
|
///
|
||||||
/// query 非空时只返回文件名包含关键词的结果
|
/// query 非空时只返回文件名包含关键词的结果。各目录并行扫描。
|
||||||
pub fn scan_tools(paths: Vec<String>, query: String) -> Result<Vec<ToolGroup>, String> {
|
pub fn scan_tools(paths: Vec<String>, query: String) -> Result<Vec<ToolGroup>, String> {
|
||||||
let query_lower = query.to_lowercase();
|
let query_lower = query.to_lowercase();
|
||||||
let mut groups: Vec<ToolGroup> = Vec::new();
|
|
||||||
|
|
||||||
for dir in &paths {
|
// 并行扫描各目录
|
||||||
|
let dir_results: Vec<(String, Option<Vec<String>>)> = std::thread::scope(|s| {
|
||||||
|
let handles: Vec<_> = paths.iter().map(|dir| {
|
||||||
|
s.spawn(move || {
|
||||||
let p = Path::new(dir);
|
let p = Path::new(dir);
|
||||||
if !p.is_dir() {
|
if !p.is_dir() {
|
||||||
groups.push(ToolGroup {
|
return (dir.clone(), None);
|
||||||
dir: dir.clone(),
|
}
|
||||||
exists: false,
|
let exes = list_exes(dir);
|
||||||
exes: vec![],
|
(dir.clone(), Some(exes))
|
||||||
|
})
|
||||||
|
}).collect();
|
||||||
|
handles.into_iter().map(|h| h.join().unwrap()).collect()
|
||||||
});
|
});
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let entries = fs::read_dir(p).map_err(|e| format!("无法读取目录 {}: {}", dir, e))?;
|
let mut groups: Vec<ToolGroup> = Vec::new();
|
||||||
let mut exes: Vec<String> = Vec::new();
|
for (dir, opt_exes) in dir_results {
|
||||||
|
match opt_exes {
|
||||||
for entry in entries.flatten() {
|
None => {
|
||||||
let fname = entry.file_name();
|
groups.push(ToolGroup { dir, exists: false, exes: vec![] });
|
||||||
let name = fname.to_string_lossy();
|
|
||||||
if let Some(ext) = Path::new(name.as_ref()).extension() {
|
|
||||||
let ext_lower = ext.to_ascii_lowercase();
|
|
||||||
if EXECUTABLE_EXTENSIONS.contains(&ext_lower.to_str().unwrap_or("")) {
|
|
||||||
if query_lower.is_empty() || name.to_lowercase().contains(&query_lower) {
|
|
||||||
exes.push(name.to_string());
|
|
||||||
}
|
}
|
||||||
|
Some(mut exes) => {
|
||||||
|
if !query_lower.is_empty() {
|
||||||
|
exes.retain(|name| name.to_lowercase().contains(&query_lower));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exes.sort();
|
exes.sort();
|
||||||
groups.push(ToolGroup {
|
groups.push(ToolGroup { dir, exists: true, exes });
|
||||||
dir: dir.clone(),
|
}
|
||||||
exists: true,
|
}
|
||||||
exes,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(groups)
|
Ok(groups)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
fn make_temp_dir_with_exes(prefix: &str, exe_names: &[&str]) -> std::path::PathBuf {
|
||||||
|
let dir = std::env::temp_dir().join(format!("patheditor_test_{}", prefix));
|
||||||
|
fs::create_dir_all(&dir).unwrap();
|
||||||
|
for name in exe_names {
|
||||||
|
fs::write(dir.join(name), b"fake").unwrap();
|
||||||
|
}
|
||||||
|
dir
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scan_conflicts_no_duplicates() {
|
||||||
|
let d1 = make_temp_dir_with_exes("c_a", &["a.exe"]);
|
||||||
|
let d2 = make_temp_dir_with_exes("c_b", &["b.exe"]);
|
||||||
|
let paths = vec![d1.to_string_lossy().to_string(), d2.to_string_lossy().to_string()];
|
||||||
|
let conflicts = scan_conflicts(paths).unwrap();
|
||||||
|
assert!(conflicts.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scan_conflicts_detects_duplicate() {
|
||||||
|
let d1 = make_temp_dir_with_exes("c_dup1", &["shared.exe"]);
|
||||||
|
let d2 = make_temp_dir_with_exes("c_dup2", &["shared.exe"]);
|
||||||
|
let paths = vec![d1.to_string_lossy().to_string(), d2.to_string_lossy().to_string()];
|
||||||
|
let conflicts = scan_conflicts(paths).unwrap();
|
||||||
|
assert_eq!(conflicts.len(), 1);
|
||||||
|
assert_eq!(conflicts[0].locations.len(), 2);
|
||||||
|
assert_eq!(conflicts[0].locations[0].priority, 0);
|
||||||
|
assert_eq!(conflicts[0].locations[1].priority, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scan_tools_returns_groups() {
|
||||||
|
let d1 = make_temp_dir_with_exes("t_a", &["tool.exe", "helper.bat"]);
|
||||||
|
let paths = vec![d1.to_string_lossy().to_string()];
|
||||||
|
let groups = scan_tools(paths, String::new()).unwrap();
|
||||||
|
assert_eq!(groups.len(), 1);
|
||||||
|
assert!(groups[0].exists);
|
||||||
|
assert!(groups[0].exes.contains(&"helper.bat".to_string()));
|
||||||
|
assert!(groups[0].exes.contains(&"tool.exe".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scan_tools_with_query_filters() {
|
||||||
|
let d1 = make_temp_dir_with_exes("t_q", &["apple.exe", "banana.exe"]);
|
||||||
|
let paths = vec![d1.to_string_lossy().to_string()];
|
||||||
|
let groups = scan_tools(paths, "apple".into()).unwrap();
|
||||||
|
assert_eq!(groups[0].exes.len(), 1);
|
||||||
|
assert_eq!(groups[0].exes[0], "apple.exe");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+19
-4
@@ -1,7 +1,12 @@
|
|||||||
use winreg::enums::*;
|
use winreg::enums::*;
|
||||||
use winreg::RegKey;
|
use winreg::RegKey;
|
||||||
|
|
||||||
/// 检测当前进程是否有管理员权限(尝试写入系统注册表键)
|
/// 检测当前进程是否有管理员权限
|
||||||
|
///
|
||||||
|
/// 通过尝试以写入权限打开系统 PATH 注册表键判断。
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// `true` 表示有管理员权限,`false` 为只读模式
|
||||||
pub fn check_admin() -> bool {
|
pub fn check_admin() -> bool {
|
||||||
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
|
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
|
||||||
hklm.open_subkey_with_flags(
|
hklm.open_subkey_with_flags(
|
||||||
@@ -13,7 +18,6 @@ pub fn check_admin() -> bool {
|
|||||||
|
|
||||||
/// 验证路径是否存在于文件系统中(且是目录)
|
/// 验证路径是否存在于文件系统中(且是目录)
|
||||||
/// 包含 % 的路径(环境变量路径)无法验证,返回 true
|
/// 包含 % 的路径(环境变量路径)无法验证,返回 true
|
||||||
|
|
||||||
pub fn validate_path(path: &str) -> bool {
|
pub fn validate_path(path: &str) -> bool {
|
||||||
if path.contains('%') {
|
if path.contains('%') {
|
||||||
return true;
|
return true;
|
||||||
@@ -56,12 +60,23 @@ pub fn expand_env_vars(path: &str) -> String {
|
|||||||
return path.to_string();
|
return path.to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 转回 UTF-8 (去掉结尾 null)
|
// 转回 UTF-8 (去掉结尾 null),保留非法码点避免丢失路径信息
|
||||||
let len = buffer.iter().position(|&c| c == 0).unwrap_or(buffer.len());
|
let len = buffer.iter().position(|&c| c == 0).unwrap_or(buffer.len());
|
||||||
String::from_utf16_lossy(&buffer[..len])
|
decode_utf16_preserving(&buffer[..len])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 解码 UTF-16 为 String,非法码点编码为 \u{XXXX} 而非静默丢弃
|
||||||
|
fn decode_utf16_preserving(v: &[u16]) -> String {
|
||||||
|
char::decode_utf16(v.iter().copied())
|
||||||
|
.map(|r| match r {
|
||||||
|
Ok(c) => c.to_string(),
|
||||||
|
Err(e) => format!("\\u{{{:X}}}", e.unpaired_surrogate()),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 广播环境变量更改通知(WM_SETTINGCHANGE)
|
/// 广播环境变量更改通知(WM_SETTINGCHANGE)
|
||||||
|
/// 广播 `WM_SETTINGCHANGE` 通知系统环境变量已变更
|
||||||
pub fn broadcast_env_change() {
|
pub fn broadcast_env_change() {
|
||||||
const HWND_BROADCAST: isize = 0xFFFF;
|
const HWND_BROADCAST: isize = 0xFFFF;
|
||||||
const WM_SETTINGCHANGE: u32 = 0x001A;
|
const WM_SETTINGCHANGE: u32 = 0x001A;
|
||||||
|
|||||||
@@ -16,6 +16,13 @@ export function createIpcMock() {
|
|||||||
case 'expand_env_vars': return 'C:\\\\Expanded';
|
case 'expand_env_vars': return 'C:\\\\Expanded';
|
||||||
case 'read_text_file': return '';
|
case 'read_text_file': return '';
|
||||||
case 'get_appdata_dir': return 'C:\\\\appdata';
|
case 'get_appdata_dir': return 'C:\\\\appdata';
|
||||||
|
case 'scan_conflicts': return [];
|
||||||
|
case 'scan_tools': return [];
|
||||||
|
case 'list_profiles': return [];
|
||||||
|
case 'save_profile': return undefined;
|
||||||
|
case 'load_profile': return null;
|
||||||
|
case 'delete_profile': return undefined;
|
||||||
|
case 'rename_profile': return undefined;
|
||||||
default: throw new Error('Unexpected invoke: ' + cmd);
|
default: throw new Error('Unexpected invoke: ' + cmd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
import { createIpcMock } from '../mocks/ipc';
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.addInitScript(createIpcMock());
|
||||||
|
await page.goto('/');
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('打开分析对话框查看冲突和工具', async ({ page }) => {
|
||||||
|
// 点击分析按钮
|
||||||
|
await page.click('text=分析');
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// 应显示冲突和工具两个标签
|
||||||
|
await expect(page.locator('text=冲突检测')).toBeVisible();
|
||||||
|
await expect(page.locator('text=工具清单')).toBeVisible();
|
||||||
|
});
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
import { createIpcMock } from '../mocks/ipc';
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.addInitScript(createIpcMock());
|
||||||
|
await page.goto('/');
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('导出按钮可见', async ({ page }) => {
|
||||||
|
await expect(page.locator('text=导出')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('导入按钮可见', async ({ page }) => {
|
||||||
|
await expect(page.locator('text=导入')).toBeVisible();
|
||||||
|
});
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
import { createIpcMock } from '../mocks/ipc';
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.addInitScript(createIpcMock());
|
||||||
|
await page.goto('/');
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Ctrl+N 打开新建对话框', async ({ page }) => {
|
||||||
|
await page.keyboard.press('Control+n');
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
await expect(page.locator('.fixed.inset-0 input[type="text"]')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Ctrl+F 聚焦搜索框', async ({ page }) => {
|
||||||
|
await page.keyboard.press('Control+f');
|
||||||
|
const searchInput = page.locator('input[placeholder]');
|
||||||
|
await expect(searchInput).toBeFocused();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('F1 打开帮助', async ({ page }) => {
|
||||||
|
await page.keyboard.press('F1');
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
await expect(page.locator('text=快捷键')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Delete 删除选中行', async ({ page }) => {
|
||||||
|
// 先选中第一行
|
||||||
|
await page.locator('table tbody tr').first().click();
|
||||||
|
await page.keyboard.press('Delete');
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
// 应有 1 行被删除 (原 2 行剩 1 行)
|
||||||
|
await expect(page.locator('table tbody tr')).toHaveCount(1);
|
||||||
|
});
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
import { createIpcMock } from '../mocks/ipc';
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.addInitScript(createIpcMock());
|
||||||
|
await page.goto('/');
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('打开配置管理对话框', async ({ page }) => {
|
||||||
|
await page.click('text=配置');
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
await expect(page.locator('text=保存当前配置')).toBeVisible();
|
||||||
|
});
|
||||||
Generated
+760
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "patheditor",
|
"name": "patheditor",
|
||||||
"version": "4.0.0",
|
"version": "5.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "patheditor",
|
"name": "patheditor",
|
||||||
"version": "4.0.0",
|
"version": "5.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/vite": "^4.3.0",
|
"@tailwindcss/vite": "^4.3.0",
|
||||||
"@tauri-apps/api": "^2.11.0",
|
"@tauri-apps/api": "^2.11.0",
|
||||||
@@ -23,6 +23,8 @@
|
|||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"@playwright/test": "^1.60.0",
|
"@playwright/test": "^1.60.0",
|
||||||
"@tauri-apps/cli": "^2.11.2",
|
"@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/node": "^24.12.3",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
@@ -31,12 +33,71 @@
|
|||||||
"eslint-plugin-react-hooks": "^7.1.1",
|
"eslint-plugin-react-hooks": "^7.1.1",
|
||||||
"eslint-plugin-react-refresh": "^0.5.2",
|
"eslint-plugin-react-refresh": "^0.5.2",
|
||||||
"globals": "^17.6.0",
|
"globals": "^17.6.0",
|
||||||
|
"jsdom": "^29.1.1",
|
||||||
"typescript": "~6.0.2",
|
"typescript": "~6.0.2",
|
||||||
"typescript-eslint": "^8.59.2",
|
"typescript-eslint": "^8.59.2",
|
||||||
"vite": "^8.0.12",
|
"vite": "^8.0.12",
|
||||||
"vitest": "^4.1.7"
|
"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",
|
||||||
|
"integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@asamuzakjp/generational-cache": "^1.0.1",
|
||||||
|
"@csstools/css-calc": "^3.2.0",
|
||||||
|
"@csstools/css-color-parser": "^4.1.0",
|
||||||
|
"@csstools/css-parser-algorithms": "^4.0.0",
|
||||||
|
"@csstools/css-tokenizer": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@asamuzakjp/dom-selector": {
|
||||||
|
"version": "7.1.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz",
|
||||||
|
"integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@asamuzakjp/generational-cache": "^1.0.1",
|
||||||
|
"@asamuzakjp/nwsapi": "^2.3.9",
|
||||||
|
"bidi-js": "^1.0.3",
|
||||||
|
"css-tree": "^3.2.1",
|
||||||
|
"is-potential-custom-element-name": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@asamuzakjp/generational-cache": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@asamuzakjp/nwsapi": {
|
||||||
|
"version": "2.3.9",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz",
|
||||||
|
"integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
"version": "7.29.0",
|
"version": "7.29.0",
|
||||||
"resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.0.tgz",
|
"resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.0.tgz",
|
||||||
@@ -286,6 +347,159 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@bramus/specificity": {
|
||||||
|
"version": "2.4.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@bramus/specificity/-/specificity-2.4.2.tgz",
|
||||||
|
"integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"css-tree": "^3.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"specificity": "bin/cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@csstools/color-helpers": {
|
||||||
|
"version": "6.0.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@csstools/color-helpers/-/color-helpers-6.0.2.tgz",
|
||||||
|
"integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT-0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@csstools/css-calc": {
|
||||||
|
"version": "3.2.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@csstools/css-calc/-/css-calc-3.2.1.tgz",
|
||||||
|
"integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@csstools/css-parser-algorithms": "^4.0.0",
|
||||||
|
"@csstools/css-tokenizer": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@csstools/css-color-parser": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@csstools/css-color-parser/-/css-color-parser-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-eZ5XOtyhK+mggRafYUWzA0tvaYOFgdY8AkgQiCJF9qNAePnUo/zmsqqYubBBb3sQ8uNUaSKTY9s9klfRaAXL0g==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@csstools/color-helpers": "^6.0.2",
|
||||||
|
"@csstools/css-calc": "^3.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@csstools/css-parser-algorithms": "^4.0.0",
|
||||||
|
"@csstools/css-tokenizer": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@csstools/css-parser-algorithms": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@csstools/css-tokenizer": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@csstools/css-syntax-patches-for-csstree": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-wgsqt92b7C7tQhIdPNxj0n9zuUbQlvAuI1exyzeNrOKOi62SD7ren8zqszmpVREjAOqg8cD2FqYhQfAuKjk4sw==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT-0",
|
||||||
|
"peerDependencies": {
|
||||||
|
"css-tree": "^3.2.1"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"css-tree": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@csstools/css-tokenizer": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@emnapi/core": {
|
"node_modules/@emnapi/core": {
|
||||||
"version": "1.10.0",
|
"version": "1.10.0",
|
||||||
"resolved": "https://registry.npmmirror.com/@emnapi/core/-/core-1.10.0.tgz",
|
"resolved": "https://registry.npmmirror.com/@emnapi/core/-/core-1.10.0.tgz",
|
||||||
@@ -445,6 +659,24 @@
|
|||||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@exodus/bytes": {
|
||||||
|
"version": "1.15.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@exodus/bytes/-/bytes-1.15.1.tgz",
|
||||||
|
"integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@noble/hashes": "^1.8.0 || ^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@noble/hashes": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@humanfs/core": {
|
"node_modules/@humanfs/core": {
|
||||||
"version": "0.19.2",
|
"version": "0.19.2",
|
||||||
"resolved": "https://registry.npmmirror.com/@humanfs/core/-/core-0.19.2.tgz",
|
"resolved": "https://registry.npmmirror.com/@humanfs/core/-/core-0.19.2.tgz",
|
||||||
@@ -1347,6 +1579,82 @@
|
|||||||
"@tauri-apps/api": "^2.11.0"
|
"@tauri-apps/api": "^2.11.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@testing-library/dom": {
|
||||||
|
"version": "10.4.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@testing-library/dom/-/dom-10.4.1.tgz",
|
||||||
|
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/code-frame": "^7.10.4",
|
||||||
|
"@babel/runtime": "^7.12.5",
|
||||||
|
"@types/aria-query": "^5.0.1",
|
||||||
|
"aria-query": "5.3.0",
|
||||||
|
"dom-accessibility-api": "^0.5.9",
|
||||||
|
"lz-string": "^1.5.0",
|
||||||
|
"picocolors": "1.1.1",
|
||||||
|
"pretty-format": "^27.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"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",
|
||||||
|
"integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.12.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@testing-library/dom": "^10.0.0",
|
||||||
|
"@types/react": "^18.0.0 || ^19.0.0",
|
||||||
|
"@types/react-dom": "^18.0.0 || ^19.0.0",
|
||||||
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tybys/wasm-util": {
|
"node_modules/@tybys/wasm-util": {
|
||||||
"version": "0.10.2",
|
"version": "0.10.2",
|
||||||
"resolved": "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
|
"resolved": "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
|
||||||
@@ -1357,6 +1665,14 @@
|
|||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/aria-query": {
|
||||||
|
"version": "5.0.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@types/aria-query/-/aria-query-5.0.4.tgz",
|
||||||
|
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/@types/chai": {
|
"node_modules/@types/chai": {
|
||||||
"version": "5.2.3",
|
"version": "5.2.3",
|
||||||
"resolved": "https://registry.npmmirror.com/@types/chai/-/chai-5.2.3.tgz",
|
"resolved": "https://registry.npmmirror.com/@types/chai/-/chai-5.2.3.tgz",
|
||||||
@@ -1848,6 +2164,41 @@
|
|||||||
"url": "https://github.com/sponsors/epoberezkin"
|
"url": "https://github.com/sponsors/epoberezkin"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ansi-regex": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ansi-styles": {
|
||||||
|
"version": "5.2.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
||||||
|
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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",
|
||||||
|
"dependencies": {
|
||||||
|
"dequal": "^2.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/assertion-error": {
|
"node_modules/assertion-error": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmmirror.com/assertion-error/-/assertion-error-2.0.1.tgz",
|
"resolved": "https://registry.npmmirror.com/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||||
@@ -1881,6 +2232,16 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bidi-js": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/bidi-js/-/bidi-js-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"require-from-string": "^2.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "5.0.6",
|
"version": "5.0.6",
|
||||||
"resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.6.tgz",
|
"resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.6.tgz",
|
||||||
@@ -1981,6 +2342,27 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/css-tree": {
|
||||||
|
"version": "3.2.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/css-tree/-/css-tree-3.2.1.tgz",
|
||||||
|
"integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mdn-data": "2.27.1",
|
||||||
|
"source-map-js": "^1.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"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": {
|
"node_modules/csstype": {
|
||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
|
||||||
@@ -1988,6 +2370,20 @@
|
|||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/data-urls": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/data-urls/-/data-urls-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"whatwg-mimetype": "^5.0.0",
|
||||||
|
"whatwg-url": "^16.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
|
||||||
@@ -2006,6 +2402,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/decimal.js": {
|
||||||
|
"version": "10.6.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/decimal.js/-/decimal.js-10.6.0.tgz",
|
||||||
|
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/deep-is": {
|
"node_modules/deep-is": {
|
||||||
"version": "0.1.4",
|
"version": "0.1.4",
|
||||||
"resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz",
|
"resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz",
|
||||||
@@ -2013,6 +2416,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/dequal": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/dequal/-/dequal-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/detect-libc": {
|
"node_modules/detect-libc": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz",
|
"resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
@@ -2022,6 +2435,14 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dom-accessibility-api": {
|
||||||
|
"version": "0.5.16",
|
||||||
|
"resolved": "https://registry.npmmirror.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
||||||
|
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.361",
|
"version": "1.5.361",
|
||||||
"resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.361.tgz",
|
"resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.361.tgz",
|
||||||
@@ -2042,6 +2463,19 @@
|
|||||||
"node": ">=10.13.0"
|
"node": ">=10.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/entities": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/entities/-/entities-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/es-module-lexer": {
|
"node_modules/es-module-lexer": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-2.1.0.tgz",
|
"resolved": "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-2.1.0.tgz",
|
||||||
@@ -2436,6 +2870,19 @@
|
|||||||
"hermes-estree": "0.25.1"
|
"hermes-estree": "0.25.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/html-encoding-sniffer": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@exodus/bytes": "^1.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/html-parse-stringify": {
|
"node_modules/html-parse-stringify": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmmirror.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
|
"resolved": "https://registry.npmmirror.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
|
||||||
@@ -2502,6 +2949,16 @@
|
|||||||
"node": ">=0.8.19"
|
"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/is-extglob": {
|
"node_modules/is-extglob": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz",
|
"resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||||
@@ -2525,6 +2982,13 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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",
|
||||||
|
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/isexe": {
|
"node_modules/isexe": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz",
|
||||||
@@ -2548,6 +3012,57 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/jsdom": {
|
||||||
|
"version": "29.1.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/jsdom/-/jsdom-29.1.1.tgz",
|
||||||
|
"integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@asamuzakjp/css-color": "^5.1.11",
|
||||||
|
"@asamuzakjp/dom-selector": "^7.1.1",
|
||||||
|
"@bramus/specificity": "^2.4.2",
|
||||||
|
"@csstools/css-syntax-patches-for-csstree": "^1.1.3",
|
||||||
|
"@exodus/bytes": "^1.15.0",
|
||||||
|
"css-tree": "^3.2.1",
|
||||||
|
"data-urls": "^7.0.0",
|
||||||
|
"decimal.js": "^10.6.0",
|
||||||
|
"html-encoding-sniffer": "^6.0.0",
|
||||||
|
"is-potential-custom-element-name": "^1.0.1",
|
||||||
|
"lru-cache": "^11.3.5",
|
||||||
|
"parse5": "^8.0.1",
|
||||||
|
"saxes": "^6.0.0",
|
||||||
|
"symbol-tree": "^3.2.4",
|
||||||
|
"tough-cookie": "^6.0.1",
|
||||||
|
"undici": "^7.25.0",
|
||||||
|
"w3c-xmlserializer": "^5.0.0",
|
||||||
|
"webidl-conversions": "^8.0.1",
|
||||||
|
"whatwg-mimetype": "^5.0.0",
|
||||||
|
"whatwg-url": "^16.0.1",
|
||||||
|
"xml-name-validator": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || ^22.13.0 || >=24.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"canvas": "^3.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"canvas": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jsdom/node_modules/lru-cache": {
|
||||||
|
"version": "11.5.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-11.5.1.tgz",
|
||||||
|
"integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"engines": {
|
||||||
|
"node": "20 || >=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jsesc": {
|
"node_modules/jsesc": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz",
|
"resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz",
|
||||||
@@ -2894,6 +3409,17 @@
|
|||||||
"yallist": "^3.0.2"
|
"yallist": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lz-string": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/lz-string/-/lz-string-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"bin": {
|
||||||
|
"lz-string": "bin/bin.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.21",
|
"version": "0.30.21",
|
||||||
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",
|
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",
|
||||||
@@ -2903,6 +3429,23 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mdn-data": {
|
||||||
|
"version": "2.27.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/mdn-data/-/mdn-data-2.27.1.tgz",
|
||||||
|
"integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "CC0-1.0"
|
||||||
|
},
|
||||||
|
"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": {
|
"node_modules/minimatch": {
|
||||||
"version": "10.2.5",
|
"version": "10.2.5",
|
||||||
"resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.5.tgz",
|
"resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.5.tgz",
|
||||||
@@ -3022,6 +3565,19 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/parse5": {
|
||||||
|
"version": "8.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/parse5/-/parse5-8.0.1.tgz",
|
||||||
|
"integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"entities": "^8.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/path-exists": {
|
"node_modules/path-exists": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz",
|
||||||
@@ -3152,6 +3708,22 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pretty-format": {
|
||||||
|
"version": "27.5.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/pretty-format/-/pretty-format-27.5.1.tgz",
|
||||||
|
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^5.0.1",
|
||||||
|
"ansi-styles": "^5.0.0",
|
||||||
|
"react-is": "^17.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/punycode": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz",
|
||||||
@@ -3210,6 +3782,38 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-is": {
|
||||||
|
"version": "17.0.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/react-is/-/react-is-17.0.2.tgz",
|
||||||
|
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||||
|
"dev": true,
|
||||||
|
"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",
|
||||||
|
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/rolldown": {
|
"node_modules/rolldown": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmmirror.com/rolldown/-/rolldown-1.0.2.tgz",
|
"resolved": "https://registry.npmmirror.com/rolldown/-/rolldown-1.0.2.tgz",
|
||||||
@@ -3243,6 +3847,19 @@
|
|||||||
"@rolldown/binding-win32-x64-msvc": "1.0.2"
|
"@rolldown/binding-win32-x64-msvc": "1.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/saxes": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/saxes/-/saxes-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"xmlchars": "^2.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=v12.22.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/scheduler": {
|
"node_modules/scheduler": {
|
||||||
"version": "0.27.0",
|
"version": "0.27.0",
|
||||||
"resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.27.0.tgz",
|
"resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.27.0.tgz",
|
||||||
@@ -3312,6 +3929,26 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"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/symbol-tree": {
|
||||||
|
"version": "3.2.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
||||||
|
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-4.3.0.tgz",
|
"resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-4.3.0.tgz",
|
||||||
@@ -3374,6 +4011,52 @@
|
|||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tldts": {
|
||||||
|
"version": "7.4.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/tldts/-/tldts-7.4.0.tgz",
|
||||||
|
"integrity": "sha512-yHBe+zVfzNZ3QfTPW/Z6KK1G2t340gFjMHqI/4KKSt/abzYydzuCnpqdaF5gCCABby+9Yfbj59oR5F2Fd5CBzg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tldts-core": "^7.4.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"tldts": "bin/cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tldts-core": {
|
||||||
|
"version": "7.4.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/tldts-core/-/tldts-core-7.4.1.tgz",
|
||||||
|
"integrity": "sha512-sc2nGvGbixlJRHwTh/qQdPXTxJU1UDJboGPQm4d/01YUJ9r/u6aeIulQvEaxUlvKDN7hb1qCLjax+jhVAPLa/g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/tough-cookie": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/tough-cookie/-/tough-cookie-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"tldts": "^7.0.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tr46": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/tr46/-/tr46-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"punycode": "^2.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ts-api-utils": {
|
"node_modules/ts-api-utils": {
|
||||||
"version": "2.5.0",
|
"version": "2.5.0",
|
||||||
"resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
|
"resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
|
||||||
@@ -3445,6 +4128,16 @@
|
|||||||
"typescript": ">=4.8.4 <6.1.0"
|
"typescript": ">=4.8.4 <6.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/undici": {
|
||||||
|
"version": "7.26.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/undici/-/undici-7.26.0.tgz",
|
||||||
|
"integrity": "sha512-3O9Tf67pGhgOv9jM35AbhkXAKi13f3oy3aE4CSgr+TckGeY+/iu97ZXN+J7DpHPzLbVApFd1IFhcnBjREYXYcg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.18.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "7.16.0",
|
"version": "7.16.0",
|
||||||
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.16.0.tgz",
|
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.16.0.tgz",
|
||||||
@@ -3678,6 +4371,54 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/w3c-xmlserializer": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"xml-name-validator": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/webidl-conversions": {
|
||||||
|
"version": "8.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
|
||||||
|
"integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/whatwg-mimetype": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/whatwg-url": {
|
||||||
|
"version": "16.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-16.0.1.tgz",
|
||||||
|
"integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@exodus/bytes": "^1.11.0",
|
||||||
|
"tr46": "^6.0.0",
|
||||||
|
"webidl-conversions": "^8.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
|
||||||
@@ -3721,6 +4462,23 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/xml-name-validator": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xmlchars": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/xmlchars/-/xmlchars-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/yallist": {
|
"node_modules/yallist": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz",
|
"resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz",
|
||||||
|
|||||||
@@ -28,6 +28,8 @@
|
|||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"@playwright/test": "^1.60.0",
|
"@playwright/test": "^1.60.0",
|
||||||
"@tauri-apps/cli": "^2.11.2",
|
"@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/node": "^24.12.3",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
@@ -36,6 +38,7 @@
|
|||||||
"eslint-plugin-react-hooks": "^7.1.1",
|
"eslint-plugin-react-hooks": "^7.1.1",
|
||||||
"eslint-plugin-react-refresh": "^0.5.2",
|
"eslint-plugin-react-refresh": "^0.5.2",
|
||||||
"globals": "^17.6.0",
|
"globals": "^17.6.0",
|
||||||
|
"jsdom": "^29.1.1",
|
||||||
"typescript": "~6.0.2",
|
"typescript": "~6.0.2",
|
||||||
"typescript-eslint": "^8.59.2",
|
"typescript-eslint": "^8.59.2",
|
||||||
"vite": "^8.0.12",
|
"vite": "^8.0.12",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useAppStore } from '@/store/app-store';
|
import { useAppStore } from '@/store/app-store';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { version } from '../../../package.json';
|
||||||
|
|
||||||
export function TitleBar() {
|
export function TitleBar() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -13,7 +14,7 @@ export function TitleBar() {
|
|||||||
<h1 className="text-lg font-semibold">
|
<h1 className="text-lg font-semibold">
|
||||||
{isAdmin ? t('app.name') : t('app.nameReadonly')}
|
{isAdmin ? t('app.name') : t('app.nameReadonly')}
|
||||||
</h1>
|
</h1>
|
||||||
<span className="text-sm opacity-60">v4.0</span>
|
<span className="text-sm opacity-60">v{version}</span>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+38
-21
@@ -1,7 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* 导入导出模块 — 对应 C 版 import_export.c
|
* 导入导出模块 — 支持 JSON、CSV、TXT 三种格式
|
||||||
* 支持 JSON、CSV、TXT 三种格式
|
*
|
||||||
|
* 注意:Rust 端 core/src/fs.rs 有对应的导入导出实现,
|
||||||
|
* 前端使用此模块(需 ImportDialog 交互),CLI 使用 Rust 版,修改时需同步两端。
|
||||||
*/
|
*/
|
||||||
|
import { version } from '../../package.json';
|
||||||
import type { PathEntry } from './path-entry';
|
import type { PathEntry } from './path-entry';
|
||||||
|
|
||||||
export type ExportFormat = 'json' | 'csv' | 'txt';
|
export type ExportFormat = 'json' | 'csv' | 'txt';
|
||||||
@@ -23,11 +26,10 @@ export function detectExportFormat(filepath: string): ExportFormat {
|
|||||||
|
|
||||||
export function exportToJson(data: ExportData): string {
|
export function exportToJson(data: ExportData): string {
|
||||||
const obj = {
|
const obj = {
|
||||||
version: '1.0',
|
version,
|
||||||
type: 'PathEditor',
|
timestamp: new Date().toISOString(),
|
||||||
exported: new Date().toISOString(),
|
system: data.system.map(e => ({ path: e.path, enabled: e.enabled })),
|
||||||
system: data.system.map(e => e.path),
|
user: data.user.map(e => ({ path: e.path, enabled: e.enabled })),
|
||||||
user: data.user.map(e => e.path),
|
|
||||||
};
|
};
|
||||||
return JSON.stringify(obj, null, 2);
|
return JSON.stringify(obj, null, 2);
|
||||||
}
|
}
|
||||||
@@ -37,13 +39,13 @@ export function exportToJson(data: ExportData): string {
|
|||||||
export function exportToCsv(data: ExportData): string {
|
export function exportToCsv(data: ExportData): string {
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
// UTF-8 BOM
|
// UTF-8 BOM
|
||||||
lines.push('type,path');
|
lines.push('type,path,enabled');
|
||||||
|
|
||||||
for (const entry of data.system) {
|
for (const entry of data.system) {
|
||||||
lines.push(`system,${escapeCsvField(entry.path)}`);
|
lines.push(`system,${escapeCsvField(entry.path)},${entry.enabled}`);
|
||||||
}
|
}
|
||||||
for (const entry of data.user) {
|
for (const entry of data.user) {
|
||||||
lines.push(`user,${escapeCsvField(entry.path)}`);
|
lines.push(`user,${escapeCsvField(entry.path)},${entry.enabled}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return lines.join('\n') + '\n';
|
return lines.join('\n') + '\n';
|
||||||
@@ -92,10 +94,13 @@ export function importFromCsv(content: string): ImportResult {
|
|||||||
|
|
||||||
if (path.length === 0) continue;
|
if (path.length === 0) continue;
|
||||||
|
|
||||||
|
// 第三列 enabled(可选,默认 true)
|
||||||
|
const enabled = fields.length >= 3 ? fields[2].trim().toLowerCase() !== 'false' : true;
|
||||||
|
|
||||||
if (type === 'system') {
|
if (type === 'system') {
|
||||||
result.system.push({ path, enabled: true });
|
result.system.push({ path, enabled });
|
||||||
} else if (type === 'user') {
|
} else if (type === 'user') {
|
||||||
result.user.push({ path, enabled: true });
|
result.user.push({ path, enabled });
|
||||||
}
|
}
|
||||||
// 未知类型忽略
|
// 未知类型忽略
|
||||||
}
|
}
|
||||||
@@ -153,20 +158,31 @@ export function importFromJson(content: string): ImportResult {
|
|||||||
try {
|
try {
|
||||||
obj = JSON.parse(content);
|
obj = JSON.parse(content);
|
||||||
} catch {
|
} catch {
|
||||||
return result; // 无效 JSON 返回空结果,由调用方显示错误
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof obj !== 'object' || obj === null) return result;
|
if (typeof obj !== 'object' || obj === null) return result;
|
||||||
|
|
||||||
|
const parseEntry = (item: unknown): { path: string; enabled: boolean } | null => {
|
||||||
|
if (typeof item === 'string') {
|
||||||
|
const trimmed = item.trim();
|
||||||
|
return trimmed.length > 0 ? { path: trimmed, enabled: true } : null;
|
||||||
|
}
|
||||||
|
if (typeof item === 'object' && item !== null) {
|
||||||
|
const rec = item as Record<string, unknown>;
|
||||||
|
const path = typeof rec.path === 'string' ? rec.path.trim() : '';
|
||||||
|
if (path.length === 0) return null;
|
||||||
|
const enabled = typeof rec.enabled === 'boolean' ? rec.enabled : true;
|
||||||
|
return { path, enabled };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
if (Array.isArray(obj.system)) {
|
if (Array.isArray(obj.system)) {
|
||||||
result.system = obj.system
|
result.system = obj.system.map(parseEntry).filter((e): e is { path: string; enabled: boolean } => e !== null);
|
||||||
.filter((p: unknown) => typeof p === 'string' && p.trim().length > 0)
|
|
||||||
.map((p: string) => ({ path: p.trim(), enabled: true }));
|
|
||||||
}
|
}
|
||||||
if (Array.isArray(obj.user)) {
|
if (Array.isArray(obj.user)) {
|
||||||
result.user = obj.user
|
result.user = obj.user.map(parseEntry).filter((e): e is { path: string; enabled: boolean } => e !== null);
|
||||||
.filter((p: unknown) => typeof p === 'string' && p.trim().length > 0)
|
|
||||||
.map((p: string) => ({ path: p.trim(), enabled: true }));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -203,9 +219,10 @@ export function importFromContent(
|
|||||||
return importFromCsv(content);
|
return importFromCsv(content);
|
||||||
} else if (lower.endsWith('.json')) {
|
} else if (lower.endsWith('.json')) {
|
||||||
return importFromJson(content);
|
return importFromJson(content);
|
||||||
} else {
|
} else if (lower.endsWith('.txt')) {
|
||||||
// TXT 文件:所有路径放入 system(用户后续可选择目标)
|
|
||||||
return { system: importFromTxt(content), user: [] };
|
return { system: importFromTxt(content), user: [] };
|
||||||
|
} else {
|
||||||
|
throw new Error(`不支持的导入格式: ${filepath}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -98,6 +98,6 @@
|
|||||||
"deleted": "Profile \"{{name}}\" deleted"
|
"deleted": "Profile \"{{name}}\" deleted"
|
||||||
},
|
},
|
||||||
"help": {
|
"help": {
|
||||||
"content": "PathEditor v4.0 — Windows System Environment Variable (PATH) Editor\n\nFeatures:\n• Create/Edit/Delete path entries\n• Move Up/Down to adjust priority\n• One-click cleanup of invalid & duplicate paths\n• Import/Export JSON, CSV, TXT formats\n• Full Undo/Redo support\n\nShortcuts:\n• Ctrl+N New\n• Ctrl+S Save\n• Ctrl+Z Undo\n• Ctrl+Y Redo\n• Ctrl+F Search\n• Delete Delete selected\n• F1 Help\n\nAuthor: 刘航宇\nGitHub: https://github.com/LHY0125/PathEditor"
|
"content": "PathEditor v5.0 — Windows System Environment Variable (PATH) Editor\n\nFeatures:\n• Create/Edit/Delete path entries\n• Move Up/Down to adjust priority\n• One-click cleanup of invalid & duplicate paths\n• Import/Export JSON, CSV, TXT formats\n• Full Undo/Redo support\n\nShortcuts:\n• Ctrl+N New\n• Ctrl+S Save\n• Ctrl+Z Undo\n• Ctrl+Y Redo\n• Ctrl+F Search\n• Delete Delete selected\n• F1 Help\n\nAuthor: 刘航宇\nGitHub: https://github.com/LHY0125/PathEditor"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,6 +98,6 @@
|
|||||||
"deleted": "已删除配置 \"{{name}}\""
|
"deleted": "已删除配置 \"{{name}}\""
|
||||||
},
|
},
|
||||||
"help": {
|
"help": {
|
||||||
"content": "PathEditor v4.0 — Windows 系统环境变量 (PATH) 编辑器\n\n功能:\n• 新建/编辑/删除路径条目\n• 上移/下移调整优先级\n• 一键清理无效和重复路径\n• 导入/导出 JSON、CSV、TXT 格式\n• 完整撤销/重做支持\n\n快捷键:\n• Ctrl+N 新建\n• Ctrl+S 保存\n• Ctrl+Z 撤销\n• Ctrl+Y 重做\n• Ctrl+F 搜索\n• Delete 删除选中\n• F1 帮助\n\n作者: 刘航宇\nGitHub: https://github.com/LHY0125/PathEditor"
|
"content": "PathEditor v5.0 — Windows 系统环境变量 (PATH) 编辑器\n\n功能:\n• 新建/编辑/删除路径条目\n• 上移/下移调整优先级\n• 一键清理无效和重复路径\n• 导入/导出 JSON、CSV、TXT 格式\n• 完整撤销/重做支持\n\n快捷键:\n• Ctrl+N 新建\n• Ctrl+S 保存\n• Ctrl+Z 撤销\n• Ctrl+Y 重做\n• Ctrl+F 搜索\n• Delete 删除选中\n• F1 帮助\n\n作者: 刘航宇\nGitHub: https://github.com/LHY0125/PathEditor"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
// @vitest-environment jsdom
|
||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import { AnalyzeDialog } from '../../src/components/dialogs/AnalyzeDialog';
|
||||||
|
|
||||||
|
vi.mock('@tauri-apps/api/core', () => ({
|
||||||
|
invoke: vi.fn((cmd: string) => {
|
||||||
|
if (cmd === 'scan_conflicts') return Promise.resolve([]);
|
||||||
|
if (cmd === 'scan_tools') return Promise.resolve([]);
|
||||||
|
return Promise.resolve(undefined);
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/store/app-store', () => ({
|
||||||
|
useAppStore: Object.assign(
|
||||||
|
vi.fn((selector) => {
|
||||||
|
const state = { sysPaths: [], userPaths: [] };
|
||||||
|
return selector(state);
|
||||||
|
}),
|
||||||
|
{ getState: () => ({ sysPaths: [], userPaths: [] }) },
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/i18n', () => ({
|
||||||
|
default: { t: vi.fn((key: string) => key) },
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('AnalyzeDialog', () => {
|
||||||
|
it('渲染冲突检测和工具清单标签页,不崩溃', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<AnalyzeDialog open={true} onClose={() => {}} />,
|
||||||
|
);
|
||||||
|
const text = container.textContent || '';
|
||||||
|
expect(text).toContain('analyze.conflicts');
|
||||||
|
expect(text).toContain('analyze.tools');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -24,11 +24,12 @@ describe('exportToJson', () => {
|
|||||||
it('导出结构化 JSON', () => {
|
it('导出结构化 JSON', () => {
|
||||||
const json = exportToJson(sampleData);
|
const json = exportToJson(sampleData);
|
||||||
const parsed = JSON.parse(json);
|
const parsed = JSON.parse(json);
|
||||||
expect(parsed.version).toBe('1.0');
|
expect(parsed.version).toBe('5.0.0');
|
||||||
expect(parsed.type).toBe('PathEditor');
|
expect(parsed.timestamp).toBeDefined();
|
||||||
expect(parsed.system).toEqual(sampleData.system.map(e => e.path));
|
expect(parsed.system.map((e: { path: string }) => e.path)).toEqual(sampleData.system.map(e => e.path));
|
||||||
expect(parsed.user).toEqual(sampleData.user.map(e => e.path));
|
expect(parsed.user.map((e: { path: string }) => e.path)).toEqual(sampleData.user.map(e => e.path));
|
||||||
expect(parsed.exported).toBeDefined();
|
expect(parsed.system[0].enabled).toBe(true);
|
||||||
|
expect(parsed.user[0].enabled).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -54,21 +55,21 @@ describe('exportToCsv', () => {
|
|||||||
it('导出 CSV 含 BOM', () => {
|
it('导出 CSV 含 BOM', () => {
|
||||||
const csv = exportToCsv(sampleData);
|
const csv = exportToCsv(sampleData);
|
||||||
expect(csv.startsWith('')).toBe(true);
|
expect(csv.startsWith('')).toBe(true);
|
||||||
expect(csv).toContain('type,path');
|
expect(csv).toContain('type,path,enabled');
|
||||||
expect(csv).toContain('system,C:\\Windows');
|
expect(csv).toContain('system,C:\\Windows,true');
|
||||||
expect(csv).toContain('user,C:\\Users\\me\\AppData');
|
expect(csv).toContain('user,C:\\Users\\me\\AppData,true');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('CSV 字段转义', () => {
|
it('CSV 字段转义', () => {
|
||||||
const data = { system: [pe('C:\\Path,with,commas')], user: [] };
|
const data = { system: [pe('C:\\Path,with,commas')], user: [] };
|
||||||
const csv = exportToCsv(data);
|
const csv = exportToCsv(data);
|
||||||
expect(csv).toContain('"C:\\Path,with,commas"');
|
expect(csv).toContain('"C:\\Path,with,commas",true');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('CSV 双引号转义', () => {
|
it('CSV 双引号转义', () => {
|
||||||
const data = { system: [pe('Path with "quotes"')], user: [] };
|
const data = { system: [pe('Path with "quotes"')], user: [] };
|
||||||
const csv = exportToCsv(data);
|
const csv = exportToCsv(data);
|
||||||
expect(csv).toContain('"Path with ""quotes"""');
|
expect(csv).toContain('"Path with ""quotes""",true');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { importFromCsv, importFromJson, importFromTxt } from '../../src/core/import-export';
|
||||||
|
|
||||||
|
describe('导入一致性(TS 端)', () => {
|
||||||
|
it('JSON 含 system + user', () => {
|
||||||
|
const json = JSON.stringify({ system: ['C:\\a', 'C:\\b'], user: ['D:\\c'] });
|
||||||
|
const r = importFromJson(json);
|
||||||
|
expect(r.system.map(e => e.path)).toEqual(['C:\\a', 'C:\\b']);
|
||||||
|
expect(r.user.map(e => e.path)).toEqual(['D:\\c']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('CSV system/user 分类', () => {
|
||||||
|
const csv = 'type,path\nsystem,C:\\sys\nuser,D:\\usr\n';
|
||||||
|
const r = importFromCsv(csv);
|
||||||
|
expect(r.system.map(e => e.path)).toEqual(['C:\\sys']);
|
||||||
|
expect(r.user.map(e => e.path)).toEqual(['D:\\usr']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('CSV 含 BOM + header', () => {
|
||||||
|
const csv = 'type,path\nsystem,C:\\x\n';
|
||||||
|
const r = importFromCsv(csv);
|
||||||
|
expect(r.system.map(e => e.path)).toEqual(['C:\\x']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('TXT 逐行读取,跳过注释', () => {
|
||||||
|
const txt = '# comment\nC:\\a\n\nD:\\b\n';
|
||||||
|
const r = importFromTxt(txt);
|
||||||
|
expect(r.map(e => e.path)).toEqual(['C:\\a', 'D:\\b']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('JSON 空数据不崩溃', () => {
|
||||||
|
const r = importFromJson('{}');
|
||||||
|
expect(r.system).toEqual([]);
|
||||||
|
expect(r.user).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// @vitest-environment jsdom
|
||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import { MergePreview } from '../../src/components/path-list/MergePreview';
|
||||||
|
|
||||||
|
vi.mock('@/store/app-store', () => ({
|
||||||
|
useAppStore: vi.fn((selector) => {
|
||||||
|
const state = {
|
||||||
|
sysPaths: [
|
||||||
|
{ path: 'C:\\Windows', enabled: true },
|
||||||
|
{ path: 'C:\\Disabled', enabled: false },
|
||||||
|
],
|
||||||
|
userPaths: [
|
||||||
|
{ path: 'D:\\UserApp', enabled: true },
|
||||||
|
],
|
||||||
|
searchQuery: '',
|
||||||
|
};
|
||||||
|
return selector(state);
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/i18n', () => ({
|
||||||
|
default: { t: vi.fn((key: string) => key) },
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('MergePreview', () => {
|
||||||
|
it('合并显示系统+用户路径', () => {
|
||||||
|
const { container } = render(<MergePreview />);
|
||||||
|
const text = container.textContent || '';
|
||||||
|
expect(text).toContain('C:\\Windows');
|
||||||
|
expect(text).toContain('D:\\UserApp');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disabled 路径在表格中存在', () => {
|
||||||
|
const { container } = render(<MergePreview />);
|
||||||
|
const text = container.textContent || '';
|
||||||
|
expect(text).toContain('C:\\Disabled');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user