knip
Version:
Find unused files, dependencies and exports in your TypeScript and JavaScript projects
297 lines (296 loc) • 14.9 kB
JavaScript
import { CacheConsultant } from './CacheConsultant.js';
import { _getInputsFromScripts } from './binaries/index.js';
import { getFilteredScripts } from './manifest/helpers.js';
import { PluginEntries, Plugins } from './plugins.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 { isConfigPattern, toDebugString, toEntry } from './util/input.js';
import { getKeysByValue } from './util/object.js';
import { basename, dirname, extname, join } from './util/path.js';
import { getFinalEntryPaths, loadConfigForPlugin } from './util/plugin.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;
isProduction;
isStrict;
rootIgnore;
negatedWorkspacePatterns = [];
ignoredWorkspacePatterns = [];
enabledPluginsMap = initEnabledPluginsMap();
enabledPlugins = [];
enabledPluginsInAncestors;
cache;
allConfigFilePaths;
constructor({ name, dir, cwd, config, manifest, dependencies, isProduction, isStrict, rootIgnore, negatedWorkspacePatterns, ignoredWorkspacePatterns, enabledPluginsInAncestors, getReferencedInternalFilePath, isCache, cacheLocation, allConfigFilePaths, }) {
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.allConfigFilePaths = allConfigFilePaths;
this.getReferencedInternalFilePath = getReferencedInternalFilePath;
this.cache = new CacheConsultant({ name: `plugins-${name}`, isEnabled: isCache, cacheLocation });
}
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() {
const 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) {
return [patterns, 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 findDependenciesByPlugins() {
const name = 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 getInputsFromScripts = (scripts, options) => _getInputsFromScripts(scripts, { ...baseScriptOptions, ...options });
const inputs = [];
const configFiles = new Map();
const remainingPlugins = new Set(this.enabledPlugins);
const addInput = (input, containingFilePath = input.containingFilePath) => inputs.push({ ...input, containingFilePath });
const handleConfigInput = (pluginName, dependency) => {
const configFilePath = this.getReferencedInternalFilePath(dependency);
if (configFilePath) {
if (!configFiles.has(pluginName))
configFiles.set(pluginName, new Set());
configFiles.get(pluginName)?.add(configFilePath);
if (extname(dependency.specifier) !== '.json')
addInput(toEntry(dependency.specifier), dependency.containingFilePath);
}
};
for (const input of [...inputsFromManifest, ...productionInputsFromManifest]) {
if (isConfigPattern(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 hasResolveEntryPaths = typeof plugin.resolveEntryPaths === 'function';
const hasResolveConfig = typeof plugin.resolveConfig === 'function';
const hasResolve = typeof plugin.resolve === 'function';
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 remainingConfigFilePaths = configFilePaths.filter(filePath => !this.allConfigFilePaths.has(filePath));
for (const f of remainingConfigFilePaths)
if (basename(f) !== 'package.json')
this.allConfigFilePaths.add(f);
const options = {
...baseScriptOptions,
config,
configFilePath: containingFilePath,
configFileDir: cwd,
configFileName: '',
getInputsFromScripts,
};
const configEntryPaths = [];
for (const configFilePath of remainingConfigFilePaths) {
const opts = {
...options,
configFilePath,
configFileDir: dirname(configFilePath),
configFileName: basename(configFilePath),
};
if (hasResolveEntryPaths || hasResolveConfig) {
const isManifest = basename(configFilePath) === 'package.json';
const fd = isManifest ? undefined : this.cache.getFileDescriptor(configFilePath);
if (fd?.meta?.data && !fd.changed) {
if (fd.meta.data.resolveEntryPaths)
for (const id of fd.meta.data.resolveEntryPaths)
configEntryPaths.push(id);
if (fd.meta.data.resolveConfig)
for (const id of fd.meta.data.resolveConfig)
addInput(id, configFilePath);
}
else {
const config = await loadConfigForPlugin(configFilePath, plugin, opts, pluginName);
const data = {};
if (config) {
if (hasResolveEntryPaths) {
const entryPaths = (await plugin.resolveEntryPaths?.(config, opts)) ?? [];
for (const entryPath of entryPaths)
configEntryPaths.push(entryPath);
data.resolveEntryPaths = entryPaths;
}
if (hasResolveConfig) {
const inputs = (await plugin.resolveConfig?.(config, opts)) ?? [];
for (const input of inputs) {
if (isConfigPattern(input)) {
handleConfigInput(input.pluginName, { ...input, containingFilePath: configFilePath });
}
addInput(input, configFilePath);
}
data.resolveConfig = inputs;
}
if (!isManifest && fd?.changed && fd.meta)
fd.meta.data = data;
}
}
}
}
const finalEntryPaths = getFinalEntryPaths(plugin, options, configEntryPaths);
for (const id of finalEntryPaths)
addInput(id, id.containingFilePath);
if (hasResolve) {
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);
}
do {
for (const [pluginName, dependencies] of configFiles.entries()) {
configFiles.delete(pluginName);
await runPlugin(pluginName, Array.from(dependencies));
}
} while (remainingPlugins.size > 0 && configFiles.size > 0);
debugLogArray(name, 'Plugin dependencies', () => compact(inputs.map(toDebugString)));
return inputs;
}
onDispose() {
this.cache.reconcile();
}
}