UNPKG

knip

Version:

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

151 lines (150 loc) 6.87 kB
import { readFile, rm, writeFile } from 'node:fs/promises'; import { formatly } from 'formatly'; import { DEFAULT_CATALOG } from './util/catalog.js'; import { debugLogArray, debugLogObject } from './util/debug.js'; import { load, save } from './util/package-json.js'; import { extname, join } from './util/path.js'; import { removeExport } from './util/remove-export.js'; export const fix = async (issues, options) => { const fixer = new IssueFixer(options); const touchedFiles = await fixer.fixIssues(issues); if (options.isFormat) { const report = await formatly(Array.from(touchedFiles)); if (report.ran && report.result && (report.result.runner === 'virtual' || report.result.code === 0)) { debugLogArray('*', `Formatted files using ${report.formatter.name} (${report.formatter.runner})`, touchedFiles); } else { debugLogObject('*', 'Formatting files failed', report); } } }; class IssueFixer { options; constructor(options) { this.options = options; } async fixIssues(issues) { const touchedFiles = new Set(); await this.removeUnusedFiles(issues); for (const filePath of await this.removeUnusedExports(issues)) touchedFiles.add(filePath); for (const filePath of await this.removeUnusedDependencies(issues)) touchedFiles.add(filePath); for (const filePath of await this.removeUnusedCatalogEntries(issues)) touchedFiles.add(filePath); return touchedFiles; } async removeUnusedFiles(issues) { if (!this.options.isFixFiles) return; for (const issue of Object.values(issues._files).flatMap(Object.values)) { await rm(issue.filePath); issue.isFixed = true; } } async removeUnusedExports(issues) { const touchedFiles = new Set(); const types = [ ...(this.options.isFixUnusedTypes ? ['types', 'nsTypes', 'classMembers', 'enumMembers'] : []), ...(this.options.isFixUnusedExports ? ['exports', 'nsExports'] : []), ]; if (types.length === 0) return touchedFiles; const allFixes = new Map(); for (const type of types) { for (const [filePath, issueMap] of Object.entries(issues[type])) { const fixes = allFixes.get(filePath) ?? []; for (const issue of Object.values(issueMap)) fixes.push(...issue.fixes); allFixes.set(filePath, fixes); } } for (const [filePath, fixes] of allFixes) { const absFilePath = join(this.options.cwd, filePath); const sourceFileText = fixes .sort((a, b) => b[0] - a[0]) .reduce((text, [start, end, flags]) => removeExport({ text, start, end, flags }), await readFile(absFilePath, 'utf-8')); await writeFile(absFilePath, sourceFileText); touchedFiles.add(absFilePath); for (const type of types) { const issueMap = issues[type]?.[filePath]; if (issueMap) for (const issue of Object.values(issueMap)) issue.isFixed = true; } } return touchedFiles; } async removeUnusedDependencies(issues) { const touchedFiles = new Set(); if (!this.options.isFixDependencies) return touchedFiles; const filePaths = new Set([...Object.keys(issues.dependencies), ...Object.keys(issues.devDependencies)]); for (const filePath of filePaths) { const absFilePath = join(this.options.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); touchedFiles.add(absFilePath); } return touchedFiles; } async removeUnusedCatalogEntries(issues) { const touchedFiles = new Set(); if (!this.options.isFixCatalog) return touchedFiles; const filePaths = new Set(Object.keys(issues.catalog)); for (const filePath of filePaths) { if (['.yml', '.yaml'].includes(extname(filePath))) { const absFilePath = join(this.options.cwd, filePath); const fileContent = await readFile(absFilePath, 'utf-8'); const issuesForFile = Object.values(issues.catalog[filePath]); const takeLine = (issue) => issue.fixes.map(fix => fix[0]); const remove = new Set(issuesForFile.flatMap(takeLine)); const keep = (_, i) => !remove.has(i + 1); await writeFile(absFilePath, fileContent.split('\n').filter(keep).join('\n')); for (const issue of issuesForFile) issue.isFixed = true; touchedFiles.add(filePath); } else { const absFilePath = join(this.options.cwd, filePath); const pkg = await load(absFilePath); const catalog = pkg.catalog || (!Array.isArray(pkg.workspaces) && pkg.workspaces?.catalog); const catalogs = pkg.catalogs || (!Array.isArray(pkg.workspaces) && pkg.workspaces?.catalogs); for (const [key, issue] of Object.entries(issues.catalog[filePath])) { if (issue.parentSymbol === DEFAULT_CATALOG) { if (catalog) { delete catalog[issue.symbol]; issues.catalog[filePath][key].isFixed = true; } } else { if (catalogs && issue.parentSymbol) { delete catalogs[issue.parentSymbol][issue.symbol]; issues.catalog[filePath][key].isFixed = true; } } } await save(absFilePath, pkg); touchedFiles.add(absFilePath); } } return touchedFiles; } }