#!/usr/bin/env node const fs = require('fs'); const path = require('path'); const glob = require('glob'); /** * 分析未使用的图片资源 * 扫描 src/blogs/ 目录下的 .assets 文件夹中的图片文件 * 检查这些图片是否在对应的 markdown 文件中被引用 */ // 支持的图片格式 const imageExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.svg', '.webp']; // 博客目录 const blogsDir = './src/blogs'; // 获取所有assets目录中的图片文件 function getAllAssetImages() { const assetImages = []; try { // 使用 glob 匹配所有 .assets 目录下的文件 const assetFiles = glob.sync(`${blogsDir}/**/*.assets/**/*`, { nodir: true }); assetFiles.forEach(filePath => { const ext = path.extname(filePath).toLowerCase(); if (imageExtensions.includes(ext)) { assetImages.push({ fullPath: filePath, fileName: path.basename(filePath), blogName: path.dirname(filePath).split('/').pop().replace('.assets', ''), relativePath: path.relative(blogsDir, filePath) }); } }); } catch (error) { console.error('读取assets目录时出错:', error.message); } return assetImages; } // 获取所有markdown文件 function getAllMarkdownFiles() { const markdownFiles = []; try { const mdFiles = glob.sync(`${blogsDir}/*.md`); mdFiles.forEach(filePath => { markdownFiles.push({ fullPath: filePath, fileName: path.basename(filePath), blogName: path.basename(filePath, '.md') }); }); } catch (error) { console.error('读取markdown文件时出错:', error.message); } return markdownFiles; } // 读取markdown文件内容并提取图片引用 function extractImageReferences(mdFilePath) { try { const content = fs.readFileSync(mdFilePath, 'utf8'); const imageRefs = new Set(); // 匹配 ![alt](path) 格式的图片引用 const markdownImageRegex = /!\[[^\]]*\]\(([^)]+)\)/g; let match; while ((match = markdownImageRegex.exec(content)) !== null) { imageRefs.add(match[1]); } // 匹配 HTML img 标签 const htmlImageRegex = /]+src=['"]*([^'">\s]+)['"]*[^>]*>/g; while ((match = htmlImageRegex.exec(content)) !== null) { imageRefs.add(match[1]); } return Array.from(imageRefs); } catch (error) { console.error(`读取文件 ${mdFilePath} 时出错:`, error.message); return []; } } // 检查图片是否被使用 function isImageUsed(imagePath, imageFileName, blogName, allMarkdownFiles) { // 首先检查对应的markdown文件 const correspondingMdFile = allMarkdownFiles.find(md => md.blogName === blogName); if (correspondingMdFile) { const imageRefs = extractImageReferences(correspondingMdFile.fullPath); // 检查是否有任何引用匹配当前图片 const isUsed = imageRefs.some(ref => { // 处理相对路径和绝对路径 const normalizedRef = ref.replace(/^\.\//, ''); const fileName = path.basename(ref); return ( fileName === imageFileName || normalizedRef.includes(imageFileName) || ref.includes(imageFileName) || normalizedRef === imagePath || ref === imagePath ); }); if (isUsed) { return { used: true, foundIn: correspondingMdFile.fileName }; } } // 如果在对应的markdown文件中没找到,检查所有其他markdown文件 for (const mdFile of allMarkdownFiles) { if (mdFile.blogName === blogName) continue; // 已经检查过了 const imageRefs = extractImageReferences(mdFile.fullPath); const isUsed = imageRefs.some(ref => { const normalizedRef = ref.replace(/^\.\//, ''); const fileName = path.basename(ref); return ( fileName === imageFileName || normalizedRef.includes(imageFileName) || ref.includes(imageFileName) ); }); if (isUsed) { return { used: true, foundIn: mdFile.fileName }; } } return { used: false, foundIn: null }; } // 主函数 function analyzeUnusedImages() { console.log('🔍 开始分析未使用的图片资源...\n'); const assetImages = getAllAssetImages(); const markdownFiles = getAllMarkdownFiles(); console.log(`📊 统计信息:`); console.log(` - 找到 ${assetImages.length} 个图片文件`); console.log(` - 找到 ${markdownFiles.length} 个markdown文件\n`); const unusedImages = []; const usedImages = []; console.log('📝 分析结果:\n'); assetImages.forEach(image => { const usage = isImageUsed(image.relativePath, image.fileName, image.blogName, markdownFiles); if (usage.used) { usedImages.push({ ...image, foundIn: usage.foundIn }); console.log(`✅ ${image.fileName} (在 ${usage.foundIn} 中使用)`); } else { unusedImages.push(image); console.log(`❌ ${image.fileName} (未使用)`); } }); // 输出汇总 console.log('\n📋 汇总报告:'); console.log(` ✅ 已使用的图片: ${usedImages.length} 个`); console.log(` ❌ 未使用的图片: ${unusedImages.length} 个`); if (unusedImages.length > 0) { console.log('\n🗑️ 未使用的图片列表:'); unusedImages.forEach(image => { console.log(` - ${image.relativePath}`); }); // 计算未使用图片的总大小 let totalSize = 0; unusedImages.forEach(image => { try { const stats = fs.statSync(image.fullPath); totalSize += stats.size; } catch (error) { console.error(`无法获取文件大小: ${image.fullPath}`); } }); console.log(`\n💾 未使用图片总大小: ${(totalSize / 1024 / 1024).toFixed(2)} MB`); // 生成删除脚本(包含git移除操作) const deleteScript = unusedImages.map(image => { return `# 删除文件: ${image.relativePath} if [ -f "${image.fullPath}" ]; then # 检查文件是否被git跟踪 if git ls-files --error-unmatch "${image.fullPath}" >/dev/null 2>&1; then echo "从git中移除: ${image.relativePath}" git rm "${image.fullPath}" else echo "删除未跟踪文件: ${image.relativePath}" rm "${image.fullPath}" fi else echo "文件不存在,跳过: ${image.relativePath}" fi`; }).join('\n\n'); const scriptContent = `#!/bin/bash # 删除未使用的图片文件并从git中移除(如果被跟踪) # 生成时间: ${new Date().toLocaleString()} set -e # 遇到错误时退出 echo "🗑️ 开始删除未使用的图片文件..." echo "总共需要处理 ${unusedImages.length} 个文件" echo "" ${deleteScript} echo "" echo "✅ 删除操作完成!" echo "如果有文件从git中移除,请记得提交这些更改:" echo "git commit -m 'Remove unused images'" `; fs.writeFileSync('./delete-unused-images.sh', scriptContent); console.log('\n📜 已生成删除脚本: delete-unused-images.sh'); console.log(' 脚本将自动检查文件是否被git跟踪,并相应地删除或从git中移除'); console.log(' 运行 chmod +x delete-unused-images.sh && ./delete-unused-images.sh 来删除这些文件'); } else { console.log('\n🎉 太棒了!所有图片都在使用中!'); } } // 检查是否安装了 glob 包 try { require('glob'); } catch (error) { console.error('❌ 缺少依赖包 "glob"'); console.log('请运行: npm install glob'); process.exit(1); } // 运行分析 analyzeUnusedImages();