knip
Version:
Find and fix unused dependencies, exports and files in your TypeScript and JavaScript projects
135 lines (134 loc) • 6.59 kB
JavaScript
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}`);
}
}
};