knip
Version:
Find and fix unused dependencies, exports and files in your TypeScript and JavaScript projects
378 lines (377 loc) • 18.8 kB
JavaScript
import picomatch from 'picomatch';
import { CacheConsultant } from './CacheConsultant.js';
import { isDefaultPattern } from './ConfigurationChief.js';
import { _getInputsFromScripts } from './binaries/index.js';
import { ROOT_WORKSPACE_NAME } from './constants.js';
import { getFilteredScripts } from './manifest/helpers.js';
import { PluginEntries, Plugins } from './plugins.js';
import { timerify } from './util/Performance.js';
import { compact } from './util/array.js';
import { debugLogArray, debugLogObject } from './util/debug.js';
import { _glob, hasNoProductionSuffix, hasProductionSuffix, negate, prependDirToPattern } from './util/glob.js';
import { isConfig, toConfig, toDebugString, toEntry, toProductionEntry, } from './util/input.js';
import { getKeysByValue } from './util/object.js';
import { basename, dirname, join } from './util/path.js';
import { loadConfigForPlugin } from './util/plugin.js';
import { ELLIPSIS } from './util/string.js';
const nullConfig = { config: null, entry: null, project: null };
const initEnabledPluginsMap = () => Object.keys(Plugins).reduce((enabled, pluginName) => ({ ...enabled, [pluginName]: false }), {});
export class WorkspaceWorker {
name;
dir;
cwd;
config;
manifest;
dependencies;
getReferencedInternalFilePath;
findWorkspaceByFilePath;
getSourceFile;
isProduction;
isStrict;
rootIgnore;
negatedWorkspacePatterns = [];
ignoredWorkspacePatterns = [];
enabledPluginsMap = initEnabledPluginsMap();
enabledPlugins = [];
enabledPluginsInAncestors;
cache;
configFilesMap;
constructor({ name, dir, cwd, config, manifest, dependencies, isProduction, isStrict, rootIgnore, negatedWorkspacePatterns, ignoredWorkspacePatterns, enabledPluginsInAncestors, getReferencedInternalFilePath, findWorkspaceByFilePath, getSourceFile, isCache, cacheLocation, configFilesMap, }) {
this.name = name;
this.dir = dir;
this.cwd = cwd;
this.config = config;
this.manifest = manifest;
this.dependencies = dependencies;
this.isProduction = isProduction;
this.isStrict = isStrict;
this.rootIgnore = rootIgnore;
this.negatedWorkspacePatterns = negatedWorkspacePatterns;
this.ignoredWorkspacePatterns = ignoredWorkspacePatterns;
this.enabledPluginsInAncestors = enabledPluginsInAncestors;
this.configFilesMap = configFilesMap;
this.getReferencedInternalFilePath = getReferencedInternalFilePath;
this.findWorkspaceByFilePath = findWorkspaceByFilePath;
this.getSourceFile = getSourceFile;
this.cache = new CacheConsultant({ name: `plugins-${name}`, isEnabled: isCache, cacheLocation, isProduction });
this.getConfigurationHints = timerify(this.getConfigurationHints.bind(this), 'worker.getConfigurationHints');
}
async init() {
this.enabledPlugins = await this.determineEnabledPlugins();
}
async determineEnabledPlugins() {
const manifest = this.manifest;
for (const [pluginName, plugin] of PluginEntries) {
if (this.config[pluginName] === false)
continue;
if (this.cwd !== this.dir && plugin.isRootOnly)
continue;
if (this.config[pluginName]) {
this.enabledPluginsMap[pluginName] = true;
continue;
}
const isEnabledInAncestor = this.enabledPluginsInAncestors.includes(pluginName);
if (isEnabledInAncestor ||
(typeof plugin.isEnabled === 'function' &&
(await plugin.isEnabled({ cwd: this.dir, manifest, dependencies: this.dependencies, config: this.config })))) {
this.enabledPluginsMap[pluginName] = true;
}
}
return getKeysByValue(this.enabledPluginsMap, true);
}
getConfigForPlugin(pluginName) {
const config = this.config[pluginName];
return typeof config === 'undefined' || typeof config === 'boolean' ? nullConfig : config;
}
getEntryFilePatterns() {
const { entry } = this.config;
if (entry.length === 0)
return [];
const excludeProductionNegations = entry.filter(pattern => !(pattern.startsWith('!') && pattern.endsWith('!')));
return [excludeProductionNegations, this.negatedWorkspacePatterns].flat();
}
getProjectFilePatterns(projectFilePatterns) {
const { project } = this.config;
if (project.length === 0)
return [];
const excludeProductionNegations = project.filter(pattern => !(pattern.startsWith('!') && pattern.endsWith('!')));
const negatedPluginConfigPatterns = this.getPluginConfigPatterns().map(negate);
const negatedPluginProjectFilePatterns = this.getPluginProjectFilePatterns().map(negate);
return [
excludeProductionNegations,
negatedPluginConfigPatterns,
negatedPluginProjectFilePatterns,
projectFilePatterns,
this.negatedWorkspacePatterns,
].flat();
}
getPluginProjectFilePatterns(patterns = []) {
for (const [pluginName, plugin] of PluginEntries) {
const pluginConfig = this.getConfigForPlugin(pluginName);
if (this.enabledPluginsMap[pluginName]) {
const { entry, project } = pluginConfig;
patterns.push(...(project ?? entry ?? plugin.project ?? []));
}
}
return [patterns, this.negatedWorkspacePatterns].flat();
}
getPluginConfigPatterns() {
const patterns = [];
for (const [pluginName, plugin] of PluginEntries) {
const pluginConfig = this.getConfigForPlugin(pluginName);
if (this.enabledPluginsMap[pluginName] && pluginConfig) {
const { config } = pluginConfig;
patterns.push(...(config ?? plugin.config ?? []));
}
}
return patterns;
}
getPluginEntryFilePatterns(patterns) {
const negateWorkspaces = patterns.some(pattern => pattern.startsWith('**/')) ? this.negatedWorkspacePatterns : [];
return [patterns, negateWorkspaces, this.ignoredWorkspacePatterns.map(negate)].flat();
}
getProductionEntryFilePatterns(negatedTestFilePatterns) {
const entry = this.config.entry.filter(hasProductionSuffix);
if (entry.length === 0)
return [];
const negatedEntryFiles = this.config.entry.filter(hasNoProductionSuffix).map(negate);
return [entry, negatedEntryFiles, negatedTestFilePatterns, this.negatedWorkspacePatterns].flat();
}
getProductionProjectFilePatterns(negatedTestFilePatterns) {
const project = this.config.project;
if (project.length === 0)
return this.getProductionEntryFilePatterns(negatedTestFilePatterns);
const _project = this.config.project.map(pattern => {
if (!(pattern.endsWith('!') || pattern.startsWith('!')))
return negate(pattern);
return pattern;
});
const negatedEntryFiles = this.config.entry.filter(hasNoProductionSuffix).map(negate);
const negatedPluginConfigPatterns = this.getPluginConfigPatterns().map(negate);
const negatedPluginProjectFilePatterns = this.getPluginProjectFilePatterns().map(negate);
return [
_project,
negatedEntryFiles,
negatedPluginConfigPatterns,
negatedPluginProjectFilePatterns,
negatedTestFilePatterns,
this.negatedWorkspacePatterns,
].flat();
}
getConfigurationFilePatterns(pluginName) {
const plugin = Plugins[pluginName];
const pluginConfig = this.getConfigForPlugin(pluginName);
return pluginConfig.config ?? plugin.config ?? [];
}
getIgnorePatterns() {
return [...this.rootIgnore, ...this.config.ignore.map(pattern => prependDirToPattern(this.name, pattern))];
}
async runPlugins() {
const wsName = this.name;
const cwd = this.dir;
const rootCwd = this.cwd;
const manifest = this.manifest;
const containingFilePath = join(cwd, 'package.json');
const isProduction = this.isProduction;
const knownBinsOnly = false;
const manifestScriptNames = new Set(Object.keys(manifest.scripts ?? {}));
const baseOptions = { manifestScriptNames, cwd, rootCwd, containingFilePath, knownBinsOnly };
const baseScriptOptions = { ...baseOptions, manifest, isProduction, enabledPlugins: this.enabledPlugins };
const [productionScripts, developmentScripts] = getFilteredScripts(manifest.scripts ?? {});
const inputsFromManifest = _getInputsFromScripts(Object.values(developmentScripts), baseOptions);
const productionInputsFromManifest = _getInputsFromScripts(Object.values(productionScripts), baseOptions);
const hasProductionInput = (input) => productionInputsFromManifest.find(d => d.specifier === input.specifier && d.type === input.type);
const createGetInputsFromScripts = (containingFilePath) => (scripts, options) => _getInputsFromScripts(scripts, { ...baseOptions, ...options, containingFilePath });
const inputs = [];
const remainingPlugins = new Set(this.enabledPlugins);
const addInput = (input, containingFilePath = input.containingFilePath) => {
if (isConfig(input)) {
handleConfigInput(input.pluginName, { ...input, containingFilePath });
}
else {
inputs.push({ ...input, containingFilePath });
}
};
const configFilesMap = this.configFilesMap;
const configFiles = this.configFilesMap.get(wsName);
const seen = new Map();
const handleConfigInput = (pluginName, input) => {
const configFilePath = this.getReferencedInternalFilePath(input);
if (configFilePath) {
const workspace = this.findWorkspaceByFilePath(configFilePath);
if (workspace) {
const name = this.name === ROOT_WORKSPACE_NAME ? workspace.name : this.name;
if (!configFilesMap.has(name))
configFilesMap.set(name, new Map());
if (!configFilesMap.get(name)?.has(pluginName))
configFilesMap.get(name)?.set(pluginName, new Set());
configFilesMap.get(name)?.get(pluginName)?.add(configFilePath);
}
}
};
for (const input of [...inputsFromManifest, ...productionInputsFromManifest]) {
if (isConfig(input)) {
handleConfigInput(input.pluginName, { ...input, containingFilePath });
}
else {
if (!isProduction)
addInput(input, containingFilePath);
else if (isProduction && (input.production || hasProductionInput(input)))
addInput(input, containingFilePath);
}
}
const runPlugin = async (pluginName, patterns) => {
const plugin = Plugins[pluginName];
const config = this.getConfigForPlugin(pluginName);
if (!config)
return;
const label = 'config file';
const configFilePaths = await _glob({ patterns, cwd: rootCwd, dir: cwd, gitignore: false, label });
const options = {
...baseScriptOptions,
config,
configFilePath: containingFilePath,
configFileDir: cwd,
configFileName: '',
getInputsFromScripts: createGetInputsFromScripts(containingFilePath),
};
if (config.entry) {
const toInput = isProduction && plugin.production && plugin.production.length > 0 ? toProductionEntry : toEntry;
for (const id of config.entry)
addInput(toInput(id));
}
else if ((!plugin.resolveConfig && !plugin.resolveFromAST) ||
(configFilePaths.filter(path => basename(path) !== 'package.json').length === 0 &&
(!this.configFilesMap.get(wsName)?.get(pluginName) ||
this.configFilesMap.get(wsName)?.get(pluginName)?.size === 0))) {
if (plugin.entry)
for (const id of plugin.entry)
addInput(toEntry(id));
if (plugin.production)
for (const id of plugin.production)
addInput(toProductionEntry(id));
}
for (const configFilePath of configFilePaths) {
const isManifest = basename(configFilePath) === 'package.json';
const fd = isManifest ? undefined : this.cache.getFileDescriptor(configFilePath);
if (fd?.meta?.data && !fd.changed) {
const data = fd.meta.data;
if (data.resolveConfig)
for (const id of data.resolveConfig)
addInput(id, configFilePath);
if (data.resolveFromAST)
for (const id of data.resolveFromAST)
addInput(id, configFilePath);
continue;
}
const resolveOpts = {
...options,
getInputsFromScripts: createGetInputsFromScripts(configFilePath),
configFilePath,
configFileDir: dirname(configFilePath),
configFileName: basename(configFilePath),
};
const cache = {};
const key = `${wsName}:${pluginName}`;
if (plugin.resolveConfig && !seen.get(key)?.has(configFilePath)) {
if (typeof plugin.setup === 'function')
await plugin.setup(resolveOpts);
const isLoad = typeof plugin.isLoadConfig === 'function' ? plugin.isLoadConfig(resolveOpts, this.dependencies) : true;
const localConfig = isLoad && (await loadConfigForPlugin(configFilePath, plugin, resolveOpts, pluginName));
if (localConfig) {
const inputs = await plugin.resolveConfig(localConfig, resolveOpts);
for (const input of inputs)
addInput(input, configFilePath);
cache.resolveConfig = inputs;
}
if (typeof plugin.teardown === 'function')
await plugin.teardown(resolveOpts);
}
if (plugin.resolveFromAST) {
const sourceFile = this.getSourceFile(configFilePath);
const resolveASTOpts = {
...resolveOpts,
getSourceFile: this.getSourceFile,
getReferencedInternalFilePath: this.getReferencedInternalFilePath,
};
if (sourceFile) {
const inputs = plugin.resolveFromAST(sourceFile, resolveASTOpts);
for (const input of inputs)
addInput(input, configFilePath);
cache.resolveFromAST = inputs;
}
}
if (basename(configFilePath) !== 'package.json') {
addInput(toEntry(configFilePath));
addInput(toConfig(pluginName, configFilePath));
if (!seen.has(key))
seen.set(key, new Set());
seen.get(key)?.add(configFilePath);
}
if (!isManifest && fd?.changed && fd.meta)
fd.meta.data = cache;
}
if (plugin.resolve) {
const dependencies = (await plugin.resolve(options)) ?? [];
for (const id of dependencies)
addInput(id, containingFilePath);
}
};
const enabledPluginTitles = this.enabledPlugins.map(name => Plugins[name].title);
debugLogObject(this.name, 'Enabled plugins', enabledPluginTitles);
for (const pluginName of this.enabledPlugins) {
const patterns = [...this.getConfigurationFilePatterns(pluginName), ...(configFiles?.get(pluginName) ?? [])];
configFiles?.delete(pluginName);
await runPlugin(pluginName, compact(patterns));
remainingPlugins.delete(pluginName);
}
{
const configFiles = this.configFilesMap.get(wsName);
if (configFiles) {
do {
for (const [pluginName, dependencies] of configFiles.entries()) {
configFiles.delete(pluginName);
if (this.enabledPlugins.includes(pluginName))
await runPlugin(pluginName, Array.from(dependencies));
else
for (const id of dependencies)
addInput(toEntry(id));
}
} while (remainingPlugins.size > 0 && configFiles.size > 0);
}
}
debugLogArray(wsName, 'Plugin dependencies', () => compact(inputs.map(toDebugString)));
return inputs;
}
getConfigurationHints(type, patterns, filePaths, includedPaths) {
const hints = new Set();
const entries = this.config[type].filter(pattern => !pattern.startsWith('!'));
const workspaceName = this.name;
const userDefinedPatterns = entries.filter(id => !isDefaultPattern(type, id));
if (userDefinedPatterns.length === 0)
return hints;
if (filePaths.length === 0) {
const identifier = `[${entries[0]}${entries.length > 1 ? `, ${ELLIPSIS}` : ''}]`;
hints.add({ type: `${type}-empty`, identifier, workspaceName });
return hints;
}
for (const pattern of patterns) {
if (pattern.startsWith('!'))
continue;
const filePathOrPattern = join(this.dir, pattern.replace(/!$/, ''));
if (includedPaths.has(filePathOrPattern)) {
hints.add({ type: `${type}-redundant`, identifier: pattern, workspaceName });
}
else {
const matcher = picomatch(filePathOrPattern);
if (!filePaths.some(filePath => matcher(filePath))) {
hints.add({ type: `${type}-empty`, identifier: pattern, workspaceName });
}
}
}
return hints;
}
onDispose() {
this.cache.reconcile();
}
}