Files
PathEditor/src/hooks/use-path-validation.ts
T
Serendipity 9453006310
CI / 前端检查 (格式 + 类型 + Lint + 测试 + 覆盖率) (push) Has been cancelled
CI / Rust 检查 (格式 + Check + Clippy + Test) (push) Has been cancelled
chore: 同步 v5.0 基础设施完善到 v5.1
从 v5.0 cherry-pick 的开源项目基础设施改进:

新增配置文件:
- .editorconfig, .gitattributes, .prettierrc, .markdownlint.json
- commitlint.config.js

新增 GitHub 社区文件:
- .github/dependabot.yml — 依赖自动更新
- .github/CODEOWNERS — 自动 PR 审查分配
- .github/FUNDING.yml — 开源赞助入口

新增文档:
- ROADMAP.md — 路线图
- SUPPORT.md — 帮助指南
- docs/screenshots/ — 应用截图

新增 Git Hooks:
- .husky/pre-commit — lint-staged 自动格式化+修复
- .husky/commit-msg — commitlint 校验

CI 强化:
- 新增 Prettier 格式检查
- 新增 Vitest 覆盖率 + Codecov 上报
- 保留 v5.1 已有的 rust-cache + jsdom 全局环境

修复:
- index.html 标题 v4.0 → v5.1
- PathEditDialog set-state-in-effect 改用 useRef prevOpen 守卫
- merge-preview.test.tsx no-explicit-any 修复
- 所有 TS/TSX 文件 Prettier 格式化统一

v5.1 保留特性:
- @tanstack/react-virtual 虚拟滚动
- jsdom 全局测试环境
- Swatinem/rust-cache CI 加速
- 105 测试全部通过
2026-06-19 19:24:03 +08:00

120 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect, useRef, useMemo } from 'react';
import { invoke } from '@tauri-apps/api/core';
import type { PathEntry } from '@/core/path-entry';
export type ValidationState = 'valid' | 'invalid' | 'unknown';
/**
* 异步验证路径目录是否真实存在 + 展开环境变量
* 缓存结果避免重复 IPC 调用。
* setState 仅在异步 .then() 回调中调用(符合 React 规则),
* 不存在路径的缓存清理通过 useMemo 派生。
*/
export function usePathValidation(paths: readonly PathEntry[]) {
const validatedRef = useRef<Set<string>>(new Set());
const expandedRef = useRef<Set<string>>(new Set());
const [validationCache, setValidationCache] = useState<Map<string, ValidationState>>(new Map());
const [expandedCache, setExpandedCache] = useState<Map<string, string>>(new Map());
// 仅保留当前 paths 中存在的条目(派生 state,不在 effect 中同步 setState
const currentKeys = useMemo(() => new Set(paths.map((p) => p.path)), [paths]);
const cleanedValidationCache = useMemo(() => {
const next = new Map(validationCache);
let changed = false;
for (const key of next.keys()) {
if (!currentKeys.has(key)) {
next.delete(key);
changed = true;
}
}
return changed ? next : validationCache;
}, [validationCache, currentKeys]);
const cleanedExpandedCache = useMemo(() => {
const next = new Map(expandedCache);
let changed = false;
for (const key of next.keys()) {
if (!currentKeys.has(key)) {
next.delete(key);
changed = true;
}
}
return changed ? next : expandedCache;
}, [expandedCache, currentKeys]);
// 同步清理 refref 不能在 render 期间修改,放在 effect 中不 setState 是安全的)
useEffect(() => {
for (const key of validatedRef.current) {
if (!currentKeys.has(key)) validatedRef.current.delete(key);
}
for (const key of expandedRef.current) {
if (!currentKeys.has(key)) expandedRef.current.delete(key);
}
}, [currentKeys]);
// 异步验证路径(setState 在 .then() 回调中,符合 React 规则)
useEffect(() => {
let cancelled = false;
const toValidate = paths.filter((p) => !validatedRef.current.has(p.path));
if (toValidate.length === 0) return;
const batch = toValidate.slice(0, 20);
Promise.all(
batch.map(async (p): Promise<[string, ValidationState]> => {
try {
if (p.path.includes('%')) return [p.path, 'valid'];
const valid: boolean = await invoke('validate_path', { path: p.path });
return [p.path, valid ? 'valid' : 'invalid'];
} catch {
return [p.path, 'unknown'];
}
}),
).then((results) => {
if (cancelled) return;
for (const [p] of results) validatedRef.current.add(p);
setValidationCache((prev) => {
const next = new Map(prev);
for (const [p, v] of results) next.set(p, v);
return next;
});
});
return () => {
cancelled = true;
};
}, [paths]);
// 异步展开环境变量(setState 在 .then() 回调中)
useEffect(() => {
let cancelled = false;
const toExpand = paths.filter((p) => p.path.includes('%') && !expandedRef.current.has(p.path));
if (toExpand.length === 0) return;
const batch = toExpand.slice(0, 20);
Promise.all(
batch.map(async (p): Promise<[string, string]> => {
try {
const expanded: string = await invoke('expand_env_vars', { path: p.path });
return [p.path, expanded !== p.path ? expanded : ''];
} catch {
return [p.path, ''];
}
}),
).then((results) => {
if (cancelled) return;
for (const [p] of results) expandedRef.current.add(p);
setExpandedCache((prev) => {
const next = new Map(prev);
for (const [p, v] of results) next.set(p, v);
return next;
});
});
return () => {
cancelled = true;
};
}, [paths]);
return { validationCache: cleanedValidationCache, expandedCache: cleanedExpandedCache };
}