UNPKG

knip

Version:

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

135 lines (134 loc) 6.59 kB
import { relative, toRelative } from "../../util/path.js"; import { Table } from "../../util/table.js"; import { byPathDepth } from "../../util/workspace.js"; import { bright, dim, getColoredTitle, getDimmedTitle } from "./util.js"; const getWorkspaceName = (hint) => hint.workspaceName && hint.workspaceName !== '.' && hint.type !== 'top-level-unconfigured' && hint.type !== 'workspace-unconfigured' && hint.type !== 'package-entry' ? hint.workspaceName : ''; const getIdentifier = (hint) => { if (hint.identifier === '.') return `. ${dim('(root)')}`; if (hint.identifier instanceof RegExp) return hint.identifier.source.replaceAll('\\/', '/'); return hint.identifier.toString(); }; const getTableForHints = (hints) => { const table = new Table({ truncateStart: ['identifier', 'workspace', 'filePath'] }); for (const hint of hints) { table.row(); table.cell('identifier', getIdentifier(hint)); table.cell('workspace', getWorkspaceName(hint)); table.cell('filePath', hint.filePath); table.cell('description', dim(hint.message)); } return table; }; const type = (id) => bright(id.split('-').at(0)); const unused = (options) => `Remove from ${type(options.type)}`; const empty = (options) => `Refine ${type(options.type)} pattern (no matches)`; const remove = (options) => `Remove redundant ${type(options.type)} pattern`; const topLevel = (options) => `Remove, or move unused top-level ${type(options.type)} to one of ${bright('"workspaces"')}`; const add = (options) => options.configFilePath ? `Add ${bright('entry')} and/or refine ${bright('project')} files (${options.size} unused files)` : `Create ${bright('knip.json')} configuration file, and add ${bright('entry')} and/or refine ${bright('project')} files (${options.size} unused files)`; const addWorkspace = (options) => options.configFilePath ? `Add ${bright('entry')} and/or refine ${bright('project')} files in ${bright(`workspaces["${options.workspaceName}"]`)} (${options.size} unused files)` : `Create ${bright('knip.json')} configuration file with ${bright(`workspaces["${options.workspaceName}"]`)} object (${options.size} unused files)`; const packageEntry = () => 'Package entry file not found'; const hintPrinters = new Map([ ['ignore', { print: unused }], ['ignoreFiles', { print: unused }], ['ignoreBinaries', { print: unused }], ['ignoreDependencies', { print: unused }], ['ignoreUnresolved', { print: unused }], ['ignoreWorkspaces', { print: unused }], ['entry-empty', { print: empty }], ['project-empty', { print: empty }], ['entry-redundant', { print: remove }], ['project-redundant', { print: remove }], ['top-level-unconfigured', { print: add }], ['workspace-unconfigured', { print: addWorkspace }], ['entry-top-level', { print: topLevel }], ['project-top-level', { print: topLevel }], ['package-entry', { print: packageEntry }], ]); export { hintPrinters }; const hintTypesOrder = [ ['top-level-unconfigured', 'workspace-unconfigured'], ['entry-top-level', 'project-top-level'], ['ignore', 'ignoreFiles'], ['ignoreWorkspaces'], ['ignoreDependencies'], ['ignoreBinaries'], ['ignoreUnresolved'], ['entry-empty', 'project-empty', 'entry-redundant', 'project-redundant'], ['package-entry'], ]; export const finalizeConfigurationHints = (results, options) => { if (results.counters.files > 20) { const workspaces = results.includedWorkspaceDirs .sort(byPathDepth) .reverse() .map(dir => ({ dir, size: 0 })); for (const filePath of results.issues.files) { const workspace = workspaces.find(ws => filePath.startsWith(ws.dir)); if (workspace) workspace.size++; } if (workspaces.length === 1) { results.configurationHints.push({ type: 'top-level-unconfigured', identifier: '.', size: workspaces[0].size }); } else { const topWorkspaces = workspaces.sort((a, b) => b.size - a.size).filter(ws => ws.size > 1); for (const { dir, size } of topWorkspaces) { const identifier = toRelative(dir, options.cwd); results.configurationHints.push({ type: 'workspace-unconfigured', workspaceName: identifier, identifier, size, }); } } } const hintsByType = new Map(); for (const hint of results.configurationHints) { const hints = hintsByType.get(hint.type) ?? []; hintsByType.set(hint.type, [...hints, hint]); } return hintTypesOrder.flatMap(hintTypes => hintTypes.flatMap(hintType => { const hints = hintsByType.get(hintType) ?? []; const topHints = hints.length > 10 ? Array.from(hints).slice(0, 10) : hints; const row = topHints.map(hint => { hint.filePath = relative(options.cwd, hint.filePath ?? options.configFilePath ?? ''); const hintPrinter = hintPrinters.get(hint.type); const message = hintPrinter ? hintPrinter.print({ ...hint, configFilePath: options.configFilePath }) : ''; return { ...hint, message }; }); if (hints.length !== topHints.length) { const more = hints.length - topHints.length; row.push({ type: hintType, identifier: `...${more} more similar hints`, filePath: '', message: '' }); } return row; })); }; export const printConfigurationHints = ({ cwd, counters, issues, tagHints, configurationHints, enabledPlugins, isTreatConfigHintsAsErrors, includedWorkspaceDirs, selectedWorkspaces, configFilePath, }) => { const rows = finalizeConfigurationHints({ issues, counters, configurationHints, tagHints, includedWorkspaceDirs, selectedWorkspaces, enabledPlugins }, { cwd, configFilePath }); if (rows.length > 0) { const getTitle = isTreatConfigHintsAsErrors ? getColoredTitle : getDimmedTitle; console.log(getTitle('Configuration hints', configurationHints.length)); console.warn(getTableForHints(rows).toString()); } if (tagHints.size > 0) { console.log(getDimmedTitle('Tag hints', tagHints.size)); for (const hint of tagHints) { const { filePath, identifier, tagName } = hint; const message = `Unused tag in ${toRelative(filePath, cwd)}:`; console.warn(dim(message), `${identifier}${tagName}`); } } };