UNPKG

knip

Version:

Find unused files, dependencies and exports in your TypeScript and JavaScript projects

105 lines (104 loc) 4.59 kB
import { readFile, rm, writeFile } from 'node:fs/promises'; import { load, save } from './util/package-json.js'; import { join, relative } from './util/path.js'; import { removeExport } from './util/remove-export.js'; export class IssueFixer { isEnabled = false; cwd = process.cwd(); isFixFiles = true; isFixDependencies = true; isFixUnusedTypes = true; isFixUnusedExports = true; unusedTypeNodes = new Map(); unusedExportNodes = new Map(); constructor({ isEnabled, cwd, fixTypes = [], isRemoveFiles }) { this.isEnabled = isEnabled; this.cwd = cwd; this.isFixFiles = isRemoveFiles && (fixTypes.length === 0 || fixTypes.includes('files')); this.isFixDependencies = fixTypes.length === 0 || fixTypes.includes('dependencies'); this.isFixUnusedTypes = fixTypes.length === 0 || fixTypes.includes('types'); this.isFixUnusedExports = fixTypes.length === 0 || fixTypes.includes('exports'); } addUnusedTypeNode(filePath, fixes) { if (!fixes || fixes.length === 0) return; if (this.unusedTypeNodes.has(filePath)) for (const fix of fixes) this.unusedTypeNodes.get(filePath)?.add(fix); else this.unusedTypeNodes.set(filePath, new Set(fixes)); } addUnusedExportNode(filePath, fixes) { if (!fixes || fixes.length === 0) return; if (this.unusedExportNodes.has(filePath)) for (const fix of fixes) this.unusedExportNodes.get(filePath)?.add(fix); else this.unusedExportNodes.set(filePath, new Set(fixes)); } async fixIssues(issues) { await this.removeUnusedFiles(issues); await this.removeUnusedExports(issues); await this.removeUnusedDependencies(issues); } markExportFixed(issues, filePath) { const relPath = relative(filePath); const types = [ ...(this.isFixUnusedTypes ? ['types', 'nsTypes', 'classMembers', 'enumMembers'] : []), ...(this.isFixUnusedExports ? ['exports', 'nsExports'] : []), ]; for (const type of types) { for (const id in issues[type][relPath]) { issues[type][relPath][id].isFixed = true; } } } async removeUnusedFiles(issues) { if (!this.isFixFiles) return; for (const issue of issues._files) { await rm(issue.filePath); issue.isFixed = true; } } async removeUnusedExports(issues) { const filePaths = new Set([...this.unusedTypeNodes.keys(), ...this.unusedExportNodes.keys()]); for (const filePath of filePaths) { const types = (this.isFixUnusedTypes && this.unusedTypeNodes.get(filePath)) || []; const exports = (this.isFixUnusedExports && this.unusedExportNodes.get(filePath)) || []; const exportPositions = [...types, ...exports].filter(fix => fix !== undefined).sort((a, b) => b[0] - a[0]); if (exportPositions.length > 0) { const sourceFileText = exportPositions.reduce((text, [start, end, flags]) => removeExport({ text, start, end, flags }), await readFile(filePath, 'utf-8')); await writeFile(filePath, sourceFileText); this.markExportFixed(issues, filePath); } } } async removeUnusedDependencies(issues) { if (!this.isFixDependencies) return; const filePaths = new Set([...Object.keys(issues.dependencies), ...Object.keys(issues.devDependencies)]); for (const filePath of filePaths) { const absFilePath = join(this.cwd, filePath); const pkg = await load(absFilePath); if (filePath in issues.dependencies) { for (const dependency of Object.keys(issues.dependencies[filePath])) { if (pkg.dependencies) { delete pkg.dependencies[dependency]; issues.dependencies[filePath][dependency].isFixed = true; } } } if (filePath in issues.devDependencies) { for (const dependency of Object.keys(issues.devDependencies[filePath])) { if (pkg.devDependencies) { delete pkg.devDependencies[dependency]; issues.devDependencies[filePath][dependency].isFixed = true; } } } await save(absFilePath, pkg); } } }