UNPKG

assetdrain

Version:

๐Ÿงน A blazing-fast CLI to detect and clean unused assets from your codebase with interactive UX.

63 lines (62 loc) โ€ข 2.24 kB
import fs from "fs/promises"; import path from "path"; import fg from "fast-glob"; import pLimit from "p-limit"; function stripComments(input) { return (input // Remove block comments (/* ... */) .replace(/\/\*[\s\S]*?\*\//g, "") // Remove line comments (//...) .replace(/\/\/.*$/gm, "")); } export async function scanForUsages(projectRoot, assetPaths, codeExtensions = [], concurrency = 20, ignore = []) { const codePatterns = codeExtensions.map((ext) => `**/*.${ext}`); const codeFiles = await fg(codePatterns, { cwd: projectRoot, absolute: true, ignore: ["**/node_modules/**", ...ignore], onlyFiles: true, }); const usedAssets = new Set(); const limit = pLimit(concurrency); // Create reverse lookup: reference string โ†’ actual asset path const referenceMap = new Map(); for (const assetPath of assetPaths) { const relativeFromRoot = path .relative(projectRoot, assetPath) .replace(/\\/g, "/"); const filename = path.basename(assetPath).replace(/\\/g, "/"); // Handle public path mapping โ†’ /image.png let webPath = "/" + relativeFromRoot; if (relativeFromRoot.startsWith("public/")) { webPath = "/" + relativeFromRoot.replace(/^public\//, ""); } const references = [ filename, relativeFromRoot, "./" + relativeFromRoot, webPath, ]; for (const ref of references) { if (!referenceMap.has(ref)) { referenceMap.set(ref, new Set()); } referenceMap.get(ref).add(assetPath); } } await Promise.all(codeFiles.map((file) => limit(async () => { try { const raw = await fs.readFile(file, "utf8"); const content = stripComments(raw); for (const [ref, assetSet] of referenceMap.entries()) { if (content.includes(ref)) { assetSet.forEach((p) => usedAssets.add(p)); } } } catch (err) { console.error(`โš  Failed to read file: ${file}`, err); } }))); return usedAssets; }