component-prune
Version:
Detect and optionally remove unused UI components in modern frontend frameworks(React, Svelte, Solid)
100 lines (87 loc) • 2.72 kB
text/typescript
import fg from "fast-glob";
import fs from "fs/promises";
import path from "path";
import type { ComponentFile } from "./types";
const CODE_EXTENSIONS = [
"ts",
"tsx",
"js",
"jsx",
"svelte",
"vue",
"solid.js",
"astro",
"mdx",
];
const EXCLUDE_DIRS = [
"node_modules",
".next",
"dist",
"build",
".vercel",
".output",
];
export async function analyzeUsage(
components: ComponentFile[],
rootDir: string
): Promise<{ used: ComponentFile[]; unused: ComponentFile[] }> {
const codeFiles = await fg(`**/*.{${CODE_EXTENSIONS.join(",")}}`, {
cwd: rootDir,
ignore: EXCLUDE_DIRS.map((d) => `${d}/**`),
absolute: true,
});
const usedSet = new Set<string>();
const componentMap = new Map<string, ComponentFile>();
for (const c of components) {
componentMap.set(path.basename(c.filePath), c);
}
// Check for barrel file exports
const barrelFile = path.join(rootDir, "src", "components", "ui", "index.tsx");
let barrelExports: string[] = [];
try {
await fs.access(barrelFile); // Check if barrel file exists
const barrelContent = await fs.readFile(barrelFile, "utf-8");
barrelExports = components
.filter(
(c) =>
barrelContent.includes(`export * from './${c.name}'`) ||
barrelContent.includes(`export { ${c.name} } from './${c.name}'`)
)
.map((c) => c.name);
} catch {
// Barrel file doesn't exist, proceed with empty barrelExports
// console.log(`No barrel file found at: ${barrelFile}`);
barrelExports = [];
}
await Promise.all(
codeFiles.map(async (file) => {
const content = await fs.readFile(file, "utf-8");
for (const component of components) {
const baseName = path.basename(component.filePath, component.extension);
// Match direct imports
const importPattern = new RegExp(
`from\\s+['"][^'"]*${baseName}(\\.[jt]sx?|\\.svelte|\\.vue|\\.solid\\.js)?['"]`,
"g"
);
// Match imports from barrel file
const barrelImportPattern = new RegExp(
`import\\s*\\{[^}]*${baseName}[^}]*\\}\\s*from\\s*['"][^'"]*components\\/ui(\\/index)?['"]`,
"g"
);
// Match JSX usage
const jsxPattern = new RegExp(`<${baseName}[\\s/>]`, "g");
if (
importPattern.test(content) ||
barrelImportPattern.test(content) ||
jsxPattern.test(content) ||
barrelExports.includes(baseName)
) {
usedSet.add(component.filePath);
}
}
})
);
const used = components.filter((c) => usedSet.has(c.filePath));
const unused = components.filter((c) => !usedSet.has(c.filePath));
return { used, unused };
}