vite-plugin-unused-code
Version:
Vite/Rollup plugin to detect unused files and unused exports in used files
151 lines (144 loc) • 5.11 kB
JavaScript
import * as fs from 'fs';
import * as path from 'path';
import fastglob from 'fast-glob';
import micromatch from 'micromatch';
function cleanupFilePath(id) {
const searchIndex = id.indexOf("?");
return searchIndex === -1 ? id : id.slice(0, searchIndex);
}
function diff(arr1, arr2) {
return [
...arr1.filter((item) => arr2.includes(item)),
...arr2.filter((item) => arr1.includes(item))
];
}
function getModules(bundle) {
const modules = /* @__PURE__ */ new Map();
for (const chunk of Object.values(bundle)) {
const renderedModules = chunk.type === "chunk" ? chunk.modules : {};
for (const [id, data] of Object.entries(renderedModules)) {
const file = cleanupFilePath(id);
if (path.isAbsolute(file)) {
const key = path.normalize(file);
const existing = modules.get(key);
const removedExports = data.removedExports.filter((name) => name !== "__esModule");
modules.set(key, {
removedExports: existing ? diff(existing.removedExports, removedExports) : removedExports
});
}
}
}
return modules;
}
function searchGlobs(globs, cwd = process.cwd()) {
const files = fastglob.sync(globs, {
absolute: true,
cwd
});
return process.platform === "win32" ? files.map((file) => path.normalize(file)) : files;
}
function filterGlobs(files, globs, cwd = process.cwd()) {
return micromatch(
files.map((file) => path.relative(cwd, file)),
globs,
{ cwd }
).map((file) => path.normalize(path.join(cwd, file)));
}
function generateUnusedFilesMessage(unusedFiles) {
const numberOfUnusedFile = unusedFiles.length;
const shouldUsePlural = numberOfUnusedFile > 1;
return `
--------------------- Unused Files ---------------------
${numberOfUnusedFile ? `
${unusedFiles.join("\n\n")}
There are ${numberOfUnusedFile} unused file${shouldUsePlural ? "s" : ""}. Please be careful if you want to remove ${shouldUsePlural ? "them" : "it"}.
` : `Perfect, there is nothing to do.`}`;
}
function generateUnusedExportsMessage(unusedExports) {
const numberOfUnusedExport = unusedExports.map(([file, exports]) => exports.length).reduce((a, b) => a + b, 0);
const shouldUsePlural = numberOfUnusedExport > 1;
return `
--------------------- Unused Exports ---------------------
${numberOfUnusedExport ? `${unusedExports.map(([file, exports]) => `${file}
\u27F6 ${exports.join(", ")}`).join("\n\n")}
There are ${numberOfUnusedExport} unused export${shouldUsePlural ? "s" : ""}. Please be careful if you want to remove ${shouldUsePlural ? "them" : "it"}.
` : `Perfect, there is nothing to do.`}`;
}
async function ensureFile(file) {
try {
await fs.promises.mkdir(path.dirname(file), { recursive: true });
} catch {
}
}
async function writeJSON(file, data) {
await ensureFile(file);
await fs.promises.writeFile(file, JSON.stringify(data, null, 2) + "\n");
}
const unusedCodePlugin = (customOptions) => {
const options = {
patterns: ["**/*.*"],
exclude: [],
detectUnusedFiles: true,
detectUnusedExport: true,
exportJSON: false,
failOnHint: false,
...customOptions
};
return {
enforce: "post",
apply: "build",
name: "vite-plugin-unused-code",
configResolved(config) {
options.context ??= config.root;
options.log ??= config.logLevel === "silent" ? "none" : config.logLevel === "info" ? "all" : "unused";
},
generateBundle(outputOptions, bundle) {
const {
context = process.cwd(),
patterns,
exclude,
detectUnusedFiles,
detectUnusedExport,
log = "all",
exportJSON,
failOnHint
} = options;
const globs = patterns.concat(exclude.map((pattern) => `!${pattern}`));
const modules = getModules(bundle);
let unusedFiles = [];
let unusedExports = [];
if (detectUnusedFiles) {
const includedFiles = searchGlobs(globs, context);
unusedFiles = includedFiles.filter((file) => !modules.has(file));
if (log === "all" || log === "unused" && unusedFiles.length) {
this.warn(generateUnusedFilesMessage(unusedFiles));
}
}
if (detectUnusedExport) {
const usedFiles = filterGlobs([...modules.keys()], globs, context);
unusedExports = usedFiles.filter((file) => {
return modules.get(file).removedExports.length;
}).map((file) => {
return [file, modules.get(file).removedExports];
});
if (log === "all" || log === "unused" && unusedExports.length) {
this.warn(generateUnusedExportsMessage(unusedExports));
}
}
if (exportJSON) {
let exportPath = "unused-code.json";
if (typeof exportJSON === "string") {
exportPath = path.join(exportJSON, exportPath);
}
writeJSON(exportPath, {
files: unusedFiles,
exports: unusedExports
});
}
if (failOnHint && (unusedFiles.length > 0 || unusedExports.length > 0)) {
this.error("Unused code detected.");
}
}
};
};
export { unusedCodePlugin as default };