node自动删除未引用文件

49 阅读1分钟

const fs = require('fs');

const path = require('path');

const glob = require('glob');

const IMAGE_EXTENSIONS = ['.svg', '.png'];

const SOURCE_FILE_EXTENSIONS = ['.js', '.jsx', '.ts', '.tsx', '.vue', '.html', '.css', '.less'];

const isDryRun = process.argv.includes('--dry-run');

const projectRoot = process.cwd();

const allImageFiles = new Set();

const referencedImages = new Set();

function normalizePath(filePath) {

  return filePath.split(path.sep).join('/');

}

function findAllImageFiles() {

  for (const ext of IMAGE_EXTENSIONS) {

    const files = glob.sync(**/*${ext}, {

      cwd: projectRoot,

      nodir: true,

      ignore: ['node_modules/', 'dist/', 'build/**'],

    });

    for (const file of files) {

      allImageFiles.add(normalizePath(path.resolve(projectRoot, file)));

    }

  }

}

function extractImagePaths(content) {

  const result = [];

  // 匹配 url(...) 样式

  const urlRegex = /url((['"]?)([^"')]+?.(svg|png))\1)/g;

  let match;

  while ((match = urlRegex.exec(content)) !== null) {

    result.push(match[2]); // match[2] 是路径

  }

  // import ... from '...svg|png'

  const importRegex = /import\s+.*?'"['"]/g;

  while ((match = importRegex.exec(content)) !== null) {

    result.push(match[1]);

  }

  // src="...svg|png"

  const srcRegex = /src='"['"]/g;

  while ((match = srcRegex.exec(content)) !== null) {

    result.push(match[1]);

  }

  return result;

}

function findReferencedImages() {

  const sourceFiles = glob.sync(

    **/*.{${SOURCE_FILE_EXTENSIONS.map((e) => e.slice(1)).join(',')}},

    {

      cwd: projectRoot,

      nodir: true,

      ignore: ['node_modules/', 'dist/', 'build/**'],

    },

  );

  for (const relativeFilePath of sourceFiles) {

    const absFilePath = path.resolve(projectRoot, relativeFilePath);

    const dir = path.dirname(absFilePath);

    let content;

    try {

      content = fs.readFileSync(absFilePath, 'utf-8');

    } catch {

      continue;

    }

    const imagePaths = extractImagePaths(content);

    for (const imgPath of imagePaths) {

      const resolvedPath = normalizePath(path.resolve(dir, imgPath));

      if (allImageFiles.has(resolvedPath)) {

        referencedImages.add(resolvedPath);

      }

    }

  }

}

function deleteUnreferencedImages() {

  let deletedCount = 0;

  for (const imgPath of allImageFiles) {

    if (!referencedImages.has(imgPath)) {

      const relativePath = path.relative(projectRoot, imgPath);

      if (isDryRun) {

        console.log([dry-run] 🚫 Will delete: ${relativePath});

      } else {

        fs.unlinkSync(imgPath);

        console.log(🗑️ Deleted: ${relativePath});

      }

      deletedCount++;

    }

  }

  console.log(\n✅ Total ${isDryRun ? 'would be deleted' : 'deleted'}: ${deletedCount} image(s).);

}

function main() {

  console.log(🔍 Scanning for unused images${isDryRun ? ' (dry-run mode)' : ''}...\n);

  findAllImageFiles();

  findReferencedImages();

  deleteUnreferencedImages();

}

main();

// node xxxx.js --dry-run。模拟删除
// node  xx.js 真实删除