knip
Version:
Find unused files, dependencies and exports in your TypeScript and JavaScript projects
312 lines (311 loc) • 15.2 kB
JavaScript
import { isBuiltin } from 'node:module';
import { DT_SCOPE, IGNORED_DEPENDENCIES, IGNORED_GLOBAL_BINARIES, IGNORED_RUNTIME_DEPENDENCIES, IGNORE_DEFINITELY_TYPED, ROOT_WORKSPACE_NAME, } from './constants.js';
import { getDependencyMetaData } from './manifest/index.js';
import { getDefinitelyTypedFor, getPackageFromDefinitelyTyped, getPackageNameFromModuleSpecifier, isDefinitelyTyped, } from './util/modules.js';
import { findMatch, toRegexOrString } from './util/regex.js';
export class DependencyDeputy {
isProduction;
isStrict;
_manifests = new Map();
referencedDependencies;
referencedBinaries;
hostDependencies;
installedBinaries;
hasTypesIncluded;
constructor({ isProduction, isStrict }) {
this.isProduction = isProduction;
this.isStrict = isStrict;
this.referencedDependencies = new Map();
this.referencedBinaries = new Map();
this.hostDependencies = new Map();
this.installedBinaries = new Map();
this.hasTypesIncluded = new Map();
}
addWorkspace({ name, cwd, dir, manifestPath, manifest, ignoreDependencies, ignoreBinaries, }) {
const dependencies = Object.keys(manifest.dependencies ?? {});
const peerDependencies = Object.keys(manifest.peerDependencies ?? {});
const optionalDependencies = Object.keys(manifest.optionalDependencies ?? {});
const optionalPeerDependencies = manifest.peerDependenciesMeta
? peerDependencies.filter(peerDependency => manifest.peerDependenciesMeta &&
peerDependency in manifest.peerDependenciesMeta &&
manifest.peerDependenciesMeta[peerDependency].optional)
: [];
const devDependencies = Object.keys(manifest.devDependencies ?? {});
const allDependencies = [...dependencies, ...devDependencies, ...peerDependencies, ...optionalDependencies];
const packageNames = [
...dependencies,
...(this.isStrict ? peerDependencies : []),
...(this.isProduction ? [] : devDependencies),
];
const { hostDependencies, installedBinaries, hasTypesIncluded } = getDependencyMetaData({
packageNames,
dir,
cwd,
});
this.setHostDependencies(name, hostDependencies);
this.setInstalledBinaries(name, installedBinaries);
this.setHasTypesIncluded(name, hasTypesIncluded);
this._manifests.set(name, {
workspaceDir: dir,
manifestPath,
ignoreDependencies: ignoreDependencies.map(toRegexOrString),
ignoreBinaries: ignoreBinaries.map(toRegexOrString),
usedIgnoreDependencies: new Set(),
usedIgnoreBinaries: new Set(),
dependencies,
devDependencies,
peerDependencies: new Set(peerDependencies),
optionalPeerDependencies,
allDependencies: new Set(allDependencies),
});
}
getWorkspaceManifest(workspaceName) {
return this._manifests.get(workspaceName);
}
getProductionDependencies(workspaceName) {
const manifest = this._manifests.get(workspaceName);
if (!manifest)
return [];
if (this.isStrict)
return [...manifest.dependencies, ...manifest.peerDependencies];
return manifest.dependencies;
}
getDevDependencies(workspaceName) {
return this._manifests.get(workspaceName)?.devDependencies ?? [];
}
getDependencies(workspaceName) {
const manifest = this._manifests.get(workspaceName);
if (!manifest)
return new Set();
return new Set([...manifest.dependencies, ...manifest.devDependencies]);
}
setInstalledBinaries(workspaceName, installedBinaries) {
this.installedBinaries.set(workspaceName, installedBinaries);
}
getInstalledBinaries(workspaceName) {
return this.installedBinaries.get(workspaceName);
}
setHasTypesIncluded(workspaceName, hasTypesIncluded) {
this.hasTypesIncluded.set(workspaceName, hasTypesIncluded);
}
getHasTypesIncluded(workspaceName) {
return this.installedBinaries.get(workspaceName);
}
addReferencedDependency(workspaceName, packageName) {
if (!this.referencedDependencies.has(workspaceName)) {
this.referencedDependencies.set(workspaceName, new Set());
}
this.referencedDependencies.get(workspaceName)?.add(packageName);
}
addReferencedBinary(workspaceName, binaryName) {
if (!this.referencedBinaries.has(workspaceName)) {
this.referencedBinaries.set(workspaceName, new Set());
}
this.referencedBinaries.get(workspaceName)?.add(binaryName);
}
setHostDependencies(workspaceName, hostDependencies) {
this.hostDependencies.set(workspaceName, hostDependencies);
}
getHostDependenciesFor(workspaceName, dependency) {
return this.hostDependencies.get(workspaceName)?.get(dependency) ?? [];
}
getOptionalPeerDependencies(workspaceName) {
const manifest = this._manifests.get(workspaceName);
if (!manifest)
return [];
return manifest.optionalPeerDependencies;
}
maybeAddReferencedExternalDependency(workspace, packageName) {
if (isBuiltin(packageName))
return true;
if (IGNORED_RUNTIME_DEPENDENCIES.has(packageName))
return true;
if (packageName === workspace.pkgName)
return true;
const workspaceNames = this.isStrict ? [workspace.name] : [workspace.name, ...[...workspace.ancestors].reverse()];
const closestWorkspaceName = workspaceNames.find(name => this.isInDependencies(name, packageName));
const typesPackageName = !isDefinitelyTyped(packageName) && getDefinitelyTypedFor(packageName);
const closestWorkspaceNameForTypes = typesPackageName && workspaceNames.find(name => this.isInDependencies(name, typesPackageName));
if (closestWorkspaceName || closestWorkspaceNameForTypes) {
if (closestWorkspaceName)
this.addReferencedDependency(closestWorkspaceName, packageName);
if (closestWorkspaceNameForTypes)
this.addReferencedDependency(closestWorkspaceNameForTypes, typesPackageName);
return true;
}
this.addReferencedDependency(workspace.name, packageName);
return false;
}
maybeAddReferencedBinary(workspace, binaryName) {
if (IGNORED_GLOBAL_BINARIES.has(binaryName))
return true;
this.addReferencedBinary(workspace.name, binaryName);
const workspaceNames = this.isStrict ? [workspace.name] : [workspace.name, ...[...workspace.ancestors].reverse()];
for (const name of workspaceNames) {
const binaries = this.getInstalledBinaries(name);
if (binaries?.has(binaryName)) {
const dependencies = binaries.get(binaryName);
if (dependencies?.size) {
for (const dependency of dependencies)
this.addReferencedDependency(name, dependency);
return true;
}
}
}
return false;
}
isInDependencies(workspaceName, packageName) {
const manifest = this._manifests.get(workspaceName);
if (!manifest)
return false;
if (this.isStrict)
return this.getProductionDependencies(workspaceName).includes(packageName);
return manifest.allDependencies.has(packageName);
}
settleDependencyIssues() {
const dependencyIssues = [];
const devDependencyIssues = [];
const optionalPeerDependencyIssues = [];
for (const [workspace, { manifestPath: filePath }] of this._manifests.entries()) {
const referencedDependencies = this.referencedDependencies.get(workspace);
const hasTypesIncluded = this.getHasTypesIncluded(workspace);
const peerDepRecs = {};
const isReferencedDependency = (dependency, isPeerDep) => {
if (referencedDependencies?.has(dependency))
return true;
if (isPeerDep && peerDepRecs[dependency])
return false;
const [scope, typedDependency] = dependency.split('/');
if (scope === DT_SCOPE) {
const typedPackageName = getPackageFromDefinitelyTyped(typedDependency);
if (IGNORE_DEFINITELY_TYPED.has(typedPackageName))
return true;
if (hasTypesIncluded?.has(typedDependency))
return false;
const hostDependencies = [
...this.getHostDependenciesFor(workspace, dependency),
...this.getHostDependenciesFor(workspace, typedPackageName),
];
if (hostDependencies.length)
return !!hostDependencies.find(host => isReferencedDependency(host.name, true));
if (!referencedDependencies)
return false;
return referencedDependencies.has(typedPackageName);
}
const hostDependencies = this.getHostDependenciesFor(workspace, dependency);
for (const { name } of hostDependencies) {
if (!peerDepRecs[name])
peerDepRecs[name] = 1;
else
peerDepRecs[name]++;
}
return hostDependencies.some(hostDependency => (isPeerDep === false || !hostDependency.isPeerOptional) && isReferencedDependency(hostDependency.name, true));
};
const isNotReferencedDependency = (dependency) => !isReferencedDependency(dependency, false);
for (const symbol of this.getProductionDependencies(workspace).filter(isNotReferencedDependency)) {
dependencyIssues.push({ type: 'dependencies', workspace, filePath, symbol });
}
for (const symbol of this.getDevDependencies(workspace).filter(isNotReferencedDependency)) {
devDependencyIssues.push({ type: 'devDependencies', filePath, workspace, symbol });
}
for (const symbol of this.getOptionalPeerDependencies(workspace).filter(d => isReferencedDependency(d))) {
optionalPeerDependencyIssues.push({ type: 'optionalPeerDependencies', filePath, workspace, symbol });
}
}
return { dependencyIssues, devDependencyIssues, optionalPeerDependencyIssues };
}
handleIgnoredDependencies(issues, counters, type) {
for (const key in issues[type]) {
const issueSet = issues[type][key];
for (const issueKey in issueSet) {
const issue = issueSet[issueKey];
const packageName = getPackageNameFromModuleSpecifier(issue.symbol);
if (!packageName)
continue;
if (IGNORED_DEPENDENCIES.has(packageName)) {
delete issueSet[issueKey];
counters[type]--;
}
else {
const manifest = this.getWorkspaceManifest(issue.workspace);
if (manifest) {
const ignoreItem = findMatch(manifest.ignoreDependencies, packageName);
if (ignoreItem) {
delete issueSet[issueKey];
counters[type]--;
manifest.usedIgnoreDependencies.add(ignoreItem);
}
else if (issue.workspace !== ROOT_WORKSPACE_NAME) {
const manifest = this.getWorkspaceManifest(ROOT_WORKSPACE_NAME);
if (manifest) {
const ignoreItem = findMatch(manifest.ignoreDependencies, packageName);
if (ignoreItem) {
delete issueSet[issueKey];
counters[type]--;
manifest.usedIgnoreDependencies.add(ignoreItem);
}
}
}
}
}
}
}
}
handleIgnoredBinaries(issues, counters, type) {
for (const key in issues[type]) {
const issueSet = issues[type][key];
for (const issueKey in issueSet) {
const issue = issueSet[issueKey];
if (IGNORED_GLOBAL_BINARIES.has(issue.symbol)) {
delete issueSet[issueKey];
counters[type]--;
continue;
}
const manifest = this.getWorkspaceManifest(issue.workspace);
if (manifest) {
const ignoreItem = findMatch(manifest.ignoreBinaries, issue.symbol);
if (ignoreItem) {
delete issueSet[issueKey];
counters[type]--;
manifest.usedIgnoreBinaries.add(ignoreItem);
}
else {
const manifest = this.getWorkspaceManifest(ROOT_WORKSPACE_NAME);
if (manifest) {
const ignoreItem = findMatch(manifest.ignoreBinaries, issue.symbol);
if (ignoreItem) {
delete issueSet[issueKey];
counters[type]--;
manifest.usedIgnoreBinaries.add(ignoreItem);
}
}
}
}
}
}
}
removeIgnoredIssues({ issues, counters }) {
this.handleIgnoredDependencies(issues, counters, 'dependencies');
this.handleIgnoredDependencies(issues, counters, 'devDependencies');
this.handleIgnoredDependencies(issues, counters, 'optionalPeerDependencies');
this.handleIgnoredDependencies(issues, counters, 'unlisted');
this.handleIgnoredDependencies(issues, counters, 'unresolved');
this.handleIgnoredBinaries(issues, counters, 'binaries');
}
getConfigurationHints() {
const configurationHints = new Set();
for (const [workspaceName, manifest] of this._manifests.entries()) {
for (const identifier of manifest.ignoreDependencies) {
if (!manifest.usedIgnoreDependencies.has(identifier)) {
configurationHints.add({ workspaceName, identifier, type: 'ignoreDependencies' });
}
}
for (const identifier of manifest.ignoreBinaries) {
if (!manifest.usedIgnoreBinaries.has(identifier)) {
configurationHints.add({ workspaceName, identifier, type: 'ignoreBinaries' });
}
}
}
return configurationHints;
}
}