UNPKG

vite-plugin-unused-code

Version:

Vite/Rollup plugin to detect unused files and unused exports in used files

172 lines (161 loc) 5.88 kB
'use strict'; const fs = require('fs'); const path = require('path'); const fastglob = require('fast-glob'); const micromatch = require('micromatch'); function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; } function _interopNamespaceCompat(e) { if (e && typeof e === 'object' && 'default' in e) return e; const n = Object.create(null); if (e) { for (const k in e) { n[k] = e[k]; } } n.default = e; return n; } const fs__namespace = /*#__PURE__*/_interopNamespaceCompat(fs); const path__namespace = /*#__PURE__*/_interopNamespaceCompat(path); const fastglob__default = /*#__PURE__*/_interopDefaultCompat(fastglob); const micromatch__default = /*#__PURE__*/_interopDefaultCompat(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__namespace.isAbsolute(file)) { const key = path__namespace.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__default.sync(globs, { absolute: true, cwd }); return process.platform === "win32" ? files.map((file) => path__namespace.normalize(file)) : files; } function filterGlobs(files, globs, cwd = process.cwd()) { return micromatch__default( files.map((file) => path__namespace.relative(cwd, file)), globs, { cwd } ).map((file) => path__namespace.normalize(path__namespace.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__namespace.promises.mkdir(path__namespace.dirname(file), { recursive: true }); } catch { } } async function writeJSON(file, data) { await ensureFile(file); await fs__namespace.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__namespace.join(exportJSON, exportPath); } writeJSON(exportPath, { files: unusedFiles, exports: unusedExports }); } if (failOnHint && (unusedFiles.length > 0 || unusedExports.length > 0)) { this.error("Unused code detected."); } } }; }; module.exports = unusedCodePlugin;