Optimized the root .gitignore to exclude virtual environments, node modules, and temp folders to ensure clean and lightweight version tracking. Co-authored-by: Cursor <cursoragent@cursor.com>
159 lines
5.2 KiB
TypeScript
159 lines
5.2 KiB
TypeScript
import type { IncomingMessage, ServerResponse } from 'http';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
const TARGET_EXTENSIONS = ['.js', '.ts', '.jsx', '.tsx'];
|
|
|
|
async function getAllFilePaths(dirPath: string): Promise<string[]> {
|
|
let files: string[] = [];
|
|
try {
|
|
const items = await fs.promises.readdir(dirPath, { withFileTypes: true });
|
|
for (const item of items) {
|
|
const fullPath = path.join(dirPath, item.name);
|
|
if (item.isDirectory()) {
|
|
files = files.concat(await getAllFilePaths(fullPath));
|
|
} else if (item.isFile()) {
|
|
const ext = path.extname(item.name).toLowerCase();
|
|
if (TARGET_EXTENSIONS.includes(ext)) {
|
|
files.push(fullPath);
|
|
}
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error(`读取目录失败: ${dirPath}`, err);
|
|
}
|
|
return files;
|
|
}
|
|
|
|
type TextReplacement = { searchText: string };
|
|
|
|
async function countMatches(dirPath: string, searchText: string): Promise<number> {
|
|
let totalCount = 0;
|
|
const files = await getAllFilePaths(dirPath);
|
|
|
|
for (const file of files) {
|
|
try {
|
|
const content = await fs.promises.readFile(file, 'utf-8');
|
|
const count = content.split(searchText).length - 1;
|
|
if (count > 0) {
|
|
totalCount += count;
|
|
}
|
|
} catch (err) {
|
|
console.error(`无法读取文件: ${file}`, err);
|
|
}
|
|
}
|
|
|
|
return totalCount;
|
|
}
|
|
|
|
async function countMatchesBatch(dirPath: string, replacements: TextReplacement[]): Promise<Record<string, number>> {
|
|
const counts: Record<string, number> = {};
|
|
const files = await getAllFilePaths(dirPath);
|
|
|
|
const searchTexts = replacements
|
|
.map(item => item.searchText)
|
|
.filter(text => typeof text === 'string' && text.length > 0);
|
|
|
|
searchTexts.forEach(text => {
|
|
counts[text] = 0;
|
|
});
|
|
|
|
if (searchTexts.length === 0) return counts;
|
|
|
|
for (const file of files) {
|
|
try {
|
|
const content = await fs.promises.readFile(file, 'utf-8');
|
|
for (const searchText of searchTexts) {
|
|
const count = content.split(searchText).length - 1;
|
|
if (count > 0) {
|
|
counts[searchText] += count;
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error(`无法读取文件: ${file}`, err);
|
|
}
|
|
}
|
|
|
|
return counts;
|
|
}
|
|
|
|
export function handleTextReplaceCount(req: IncomingMessage, res: ServerResponse): boolean {
|
|
if (req.method === 'POST' && req.url?.startsWith('/api/text-replace/count')) {
|
|
let body = '';
|
|
|
|
req.on('data', chunk => {
|
|
body += chunk.toString();
|
|
});
|
|
|
|
req.on('end', async () => {
|
|
try {
|
|
const { path: targetPath, searchText, searchTexts, replacements } = JSON.parse(body);
|
|
|
|
if (!targetPath || (!searchText && !Array.isArray(searchTexts) && !Array.isArray(replacements))) {
|
|
res.statusCode = 400;
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.end(JSON.stringify({ error: 'path and search text data are required' }));
|
|
return;
|
|
}
|
|
|
|
const pathParts = targetPath.split('/').filter(Boolean);
|
|
if (pathParts.length < 2 || !['components', 'prototypes'].includes(pathParts[0])) {
|
|
res.statusCode = 400;
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.end(JSON.stringify({ error: 'Invalid path format. Expected: components/xxx or prototypes/xxx' }));
|
|
return;
|
|
}
|
|
|
|
const fullPath = path.resolve(process.cwd(), 'src', targetPath);
|
|
|
|
if (!fs.existsSync(fullPath)) {
|
|
res.statusCode = 404;
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.end(JSON.stringify({ error: 'Path not found' }));
|
|
return;
|
|
}
|
|
|
|
if (Array.isArray(replacements) || Array.isArray(searchTexts)) {
|
|
const list = Array.isArray(replacements)
|
|
? replacements
|
|
.filter((item: any) => item && typeof item.searchText === 'string')
|
|
.map((item: any) => ({ searchText: String(item.searchText) }))
|
|
: (searchTexts || [])
|
|
.filter((item: any) => typeof item === 'string')
|
|
.map((item: any) => ({ searchText: String(item) }));
|
|
|
|
if (list.length === 0) {
|
|
res.statusCode = 400;
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.end(JSON.stringify({ error: 'searchTexts is empty' }));
|
|
return;
|
|
}
|
|
|
|
const countsMap = await countMatchesBatch(fullPath, list);
|
|
const totalCount = Object.values(countsMap).reduce((sum, value) => sum + value, 0);
|
|
res.statusCode = 200;
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.end(JSON.stringify({ success: true, counts: countsMap, totalCount }));
|
|
return;
|
|
}
|
|
|
|
const count = await countMatches(fullPath, searchText);
|
|
console.log(`[API] 统计文本 "${searchText}" 出现次数: ${count}`);
|
|
|
|
res.statusCode = 200;
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.end(JSON.stringify({ success: true, count, totalCount: count }));
|
|
} catch (err) {
|
|
console.error('[API] ❌ 统计文本失败:', err);
|
|
res.statusCode = 500;
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.end(JSON.stringify({ error: 'Failed to count text matches' }));
|
|
}
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|