knip
Version:
Find and fix unused dependencies, exports and files in your TypeScript and JavaScript projects
332 lines (331 loc) • 17 kB
JavaScript
import { WorkspaceWorker } from '../WorkspaceWorker.js';
import { _getInputsFromScripts } from '../binaries/index.js';
import { getCompilerExtensions, getIncludedCompilers } from '../compilers/index.js';
import { DEFAULT_EXTENSIONS } from '../constants.js';
import { perfObserver } from '../util/Performance.js';
import { debugLog, debugLogArray } from '../util/debug.js';
import { getReferencedInputsHandler } from '../util/get-referenced-inputs.js';
import { _glob, negate } from '../util/glob.js';
import { isAlias, isConfig, isDeferResolveEntry, isDeferResolveProductionEntry, isEntry, isIgnore, isProductionEntry, isProject, toProductionEntry, } from '../util/input.js';
import { getOrCreateFileNode, updateImportMap } from '../util/module-graph.js';
import { getEntryPathsFromManifest } from '../util/package-json.js';
import { dirname, isAbsolute, join, relative, toRelative } from '../util/path.js';
import { augmentWorkspace, getToSourcePathHandler, getToSourcePathsHandler } from '../util/to-source-path.js';
import { loadTSConfig } from '../util/tsconfig-loader.js';
export async function build({ cacheLocation, chief, collector, cwd, deputy, factory, gitignore, isCache, isFixExports, isFixTypes, isGitIgnored, isIsolateWorkspaces, isProduction, isSkipLibs, isStrict, isWatch, report, streamer, tags, tsConfigFile, workspaces, }) {
const configFilesMap = new Map();
const enabledPluginsStore = new Map();
const toSourceFilePath = getToSourcePathHandler(chief);
const toSourceFilePaths = getToSourcePathsHandler(chief);
const getReferencedInternalFilePath = getReferencedInputsHandler(collector, deputy, chief, isGitIgnored);
const isReportClassMembers = report.classMembers;
for (const workspace of workspaces) {
const { name, dir, manifestPath, manifestStr } = workspace;
const manifest = chief.getManifestForWorkspace(name);
if (!manifest)
continue;
deputy.addWorkspace({ name, cwd, dir, manifestPath, manifestStr, manifest, ...chief.getIgnores(name) });
}
for (const workspace of workspaces) {
const { name, dir, ancestors, pkgName } = workspace;
streamer.cast('Analyzing workspace', name);
const manifest = chief.getManifestForWorkspace(name);
if (!manifest)
continue;
const dependencies = deputy.getDependencies(name);
const compilers = getIncludedCompilers(chief.config.syncCompilers, chief.config.asyncCompilers, dependencies);
const extensions = getCompilerExtensions(compilers);
const extensionGlobStr = `.{${[...DEFAULT_EXTENSIONS, ...extensions].map(ext => ext.slice(1)).join(',')}}`;
const config = chief.getConfigForWorkspace(name, extensions);
const tsConfigFilePath = join(dir, tsConfigFile ?? 'tsconfig.json');
const { isFile, compilerOptions, definitionPaths } = await loadTSConfig(tsConfigFilePath);
if (isFile)
augmentWorkspace(workspace, dir, compilerOptions);
const worker = new WorkspaceWorker({
name,
dir,
cwd,
config,
manifest,
dependencies,
getReferencedInternalFilePath: (input) => getReferencedInternalFilePath(input, workspace),
findWorkspaceByFilePath: chief.findWorkspaceByFilePath.bind(chief),
isProduction,
isStrict,
rootIgnore: chief.config.ignore,
negatedWorkspacePatterns: chief.getNegatedWorkspacePatterns(name),
ignoredWorkspacePatterns: chief.getIgnoredWorkspacesFor(name),
enabledPluginsInAncestors: ancestors.flatMap(ancestor => enabledPluginsStore.get(ancestor) ?? []),
getSourceFile: (filePath) => principal.backend.fileManager.getSourceFile(filePath),
isCache,
cacheLocation,
configFilesMap,
});
await worker.init();
const inputs = new Set();
if (definitionPaths.length > 0) {
debugLogArray(name, 'Definition paths', definitionPaths);
for (const id of definitionPaths)
inputs.add(toProductionEntry(id, { containingFilePath: tsConfigFilePath }));
}
const ignore = worker.getIgnorePatterns();
const sharedGlobOptions = { cwd, dir, gitignore };
collector.addIgnorePatterns(ignore.map(pattern => join(cwd, pattern)));
const entryPathsFromManifest = getEntryPathsFromManifest(manifest);
for (const filePath of await toSourceFilePaths(entryPathsFromManifest, dir, extensionGlobStr)) {
inputs.add(toProductionEntry(filePath));
}
const principal = factory.createPrincipal({
cwd: dir,
isFile,
compilerOptions,
compilers,
pkgName,
isIsolateWorkspaces,
isSkipLibs,
isWatch,
toSourceFilePath,
isCache,
cacheLocation,
isProduction,
});
principal.addPaths(config.paths, dir);
const inputsFromPlugins = await worker.runPlugins();
for (const id of inputsFromPlugins)
inputs.add(Object.assign(id, { skipExportsAnalysis: !id.allowIncludeExports }));
enabledPluginsStore.set(name, worker.enabledPlugins);
const entryPatterns = new Set();
const entryPatternsSkipExports = new Set();
const productionPatterns = new Set();
const productionPatternsSkipExports = new Set();
const projectFilePatterns = new Set();
for (const input of inputs) {
const specifier = input.specifier;
if (isEntry(input)) {
const relativePath = isAbsolute(specifier) ? relative(dir, specifier) : specifier;
if (!input.skipExportsAnalysis) {
entryPatterns.add(relativePath);
}
else {
entryPatternsSkipExports.add(relativePath);
}
}
else if (isProductionEntry(input)) {
const relativePath = isAbsolute(specifier) ? relative(dir, specifier) : specifier;
if (!input.skipExportsAnalysis) {
productionPatterns.add(relativePath);
}
else {
productionPatternsSkipExports.add(relativePath);
}
}
else if (isProject(input)) {
projectFilePatterns.add(isAbsolute(specifier) ? relative(dir, specifier) : specifier);
}
else if (isAlias(input)) {
principal.addPaths({ [input.specifier]: input.prefixes }, input.dir ?? dir);
}
else if (isIgnore(input)) {
if (input.issueType === 'dependencies' || input.issueType === 'unlisted') {
deputy.addIgnoredDependencies(name, input.specifier);
}
else if (input.issueType === 'binaries') {
deputy.addIgnoredBinaries(name, input.specifier);
}
}
else if (!isConfig(input)) {
const ws = (input.containingFilePath && chief.findWorkspaceByFilePath(input.containingFilePath)) || workspace;
const resolvedFilePath = getReferencedInternalFilePath(input, ws);
if (resolvedFilePath) {
if (isDeferResolveProductionEntry(input)) {
productionPatternsSkipExports.add(resolvedFilePath);
}
else if (isDeferResolveEntry(input)) {
if (!isProduction || !input.optional)
entryPatternsSkipExports.add(resolvedFilePath);
}
else {
principal.addEntryPath(resolvedFilePath, { skipExportsAnalysis: true });
}
}
}
}
if (isProduction) {
const negatedEntryPatterns = [...entryPatterns, ...entryPatternsSkipExports].map(negate);
{
const label = 'entry paths';
const patterns = worker.getProductionEntryFilePatterns(negatedEntryPatterns);
const workspaceEntryPaths = await _glob({ ...sharedGlobOptions, patterns, gitignore: false, label });
principal.addEntryPaths(workspaceEntryPaths);
}
{
const label = 'production entry paths from plugins (skip exports analysis)';
const patterns = Array.from(productionPatternsSkipExports);
const pluginWorkspaceEntryPaths = await _glob({ ...sharedGlobOptions, patterns, label });
principal.addEntryPaths(pluginWorkspaceEntryPaths, { skipExportsAnalysis: true });
}
{
const label = 'production entry paths from plugins';
const patterns = Array.from(productionPatterns);
const pluginWorkspaceEntryPaths = await _glob({ ...sharedGlobOptions, patterns, label });
principal.addEntryPaths(pluginWorkspaceEntryPaths);
}
{
const label = 'project paths';
const patterns = worker.getProductionProjectFilePatterns(negatedEntryPatterns);
const workspaceProjectPaths = await _glob({ ...sharedGlobOptions, patterns, label });
for (const projectPath of workspaceProjectPaths)
principal.addProjectPath(projectPath);
}
}
else {
{
const label = 'entry paths from plugins (skip exports analysis)';
const patterns = worker.getPluginEntryFilePatterns([
...entryPatternsSkipExports,
...productionPatternsSkipExports,
]);
const pluginWorkspaceEntryPaths = await _glob({ ...sharedGlobOptions, patterns, label });
principal.addEntryPaths(pluginWorkspaceEntryPaths, { skipExportsAnalysis: true });
}
{
const label = 'entry paths from plugins';
const patterns = worker.getPluginEntryFilePatterns([...entryPatterns, ...productionPatterns]);
const pluginWorkspaceEntryPaths = await _glob({ ...sharedGlobOptions, patterns, label });
principal.addEntryPaths(pluginWorkspaceEntryPaths);
}
{
const label = 'entry paths';
const patterns = worker.getEntryFilePatterns();
const entryPaths = await _glob({ ...sharedGlobOptions, patterns, gitignore: false, label });
const hints = worker.getConfigurationHints('entry', patterns, entryPaths, principal.entryPaths);
for (const hint of hints)
collector.addConfigurationHint(hint);
principal.addEntryPaths(entryPaths);
}
{
const label = 'project paths from plugins';
const patterns = worker.getPluginProjectFilePatterns();
const pluginWorkspaceProjectPaths = await _glob({ ...sharedGlobOptions, patterns, label });
for (const projectPath of pluginWorkspaceProjectPaths)
principal.addProjectPath(projectPath);
}
{
const label = 'plugin configuration paths (skip exports analysis)';
const patterns = worker.getPluginConfigPatterns();
const configurationEntryPaths = await _glob({ ...sharedGlobOptions, patterns, label });
principal.addEntryPaths(configurationEntryPaths, { skipExportsAnalysis: true });
}
{
const label = 'project paths';
const patterns = worker.getProjectFilePatterns([...productionPatternsSkipExports, ...projectFilePatterns]);
const projectPaths = await _glob({ ...sharedGlobOptions, patterns, label });
const hints = worker.getConfigurationHints('project', config.project, projectPaths, principal.projectPaths);
for (const hint of hints)
collector.addConfigurationHint(hint);
for (const projectPath of projectPaths)
principal.addProjectPath(projectPath);
}
}
if (chief.resolvedConfigFilePath) {
principal.addEntryPath(chief.resolvedConfigFilePath, { skipExportsAnalysis: true });
}
worker.onDispose();
}
const principals = factory.getPrincipals();
debugLog('*', `Created ${principals.length} programs for ${workspaces.length} workspaces`);
const graph = new Map();
const analyzedFiles = new Set();
const unreferencedFiles = new Set();
const entryPaths = new Set();
const isInternalWorkspace = (packageName) => chief.availableWorkspacePkgNames.has(packageName);
const getPrincipalByFilePath = (filePath) => {
const workspace = chief.findWorkspaceByFilePath(filePath);
if (workspace)
return factory.getPrincipalByPackageName(workspace.pkgName);
};
const analyzeSourceFile = (filePath, principal) => {
if (!isWatch && analyzedFiles.has(filePath))
return;
analyzedFiles.add(filePath);
const workspace = chief.findWorkspaceByFilePath(filePath);
if (workspace) {
const { imports, exports, duplicates, scripts, traceRefs } = principal.analyzeSourceFile(filePath, {
skipTypeOnly: isStrict,
isFixExports,
isFixTypes,
ignoreExportsUsedInFile: chief.config.ignoreExportsUsedInFile,
isReportClassMembers,
tags,
}, isGitIgnored, isInternalWorkspace, getPrincipalByFilePath);
const node = getOrCreateFileNode(graph, filePath);
node.imports = imports;
node.exports = exports;
node.duplicates = duplicates;
node.scripts = scripts;
node.traceRefs = traceRefs;
updateImportMap(node, imports.internal, graph);
node.internalImportCache = imports.internal;
graph.set(filePath, node);
if (scripts && scripts.size > 0) {
const dependencies = deputy.getDependencies(workspace.name);
const manifestScriptNames = new Set(Object.keys(chief.getManifestForWorkspace(workspace.name)?.scripts ?? {}));
const dir = dirname(filePath);
const options = { cwd: dir, rootCwd: cwd, containingFilePath: filePath, dependencies, manifestScriptNames };
const inputs = _getInputsFromScripts(scripts, options);
for (const input of inputs) {
input.containingFilePath ??= filePath;
input.dir ??= dir;
const specifierFilePath = getReferencedInternalFilePath(input, workspace);
if (specifierFilePath)
analyzeSourceFile(specifierFilePath, principal);
}
}
}
};
for (let i = 0; i < principals.length; ++i) {
const principal = principals[i];
if (!principal)
continue;
principal.init();
if (principal.asyncCompilers.size > 0) {
streamer.cast('Running async compilers');
await principal.runAsyncCompilers();
}
streamer.cast('Analyzing source files', toRelative(principal.cwd));
let size = principal.entryPaths.size;
let round = 0;
do {
size = principal.entryPaths.size;
const resolvedFiles = principal.getUsedResolvedFiles();
const files = resolvedFiles.filter(filePath => !analyzedFiles.has(filePath));
debugLogArray('*', `Analyzing used resolved files [P${i + 1}/${++round}]`, files);
for (const filePath of files)
analyzeSourceFile(filePath, principal);
} while (size !== principal.entryPaths.size);
for (const filePath of principal.getUnreferencedFiles())
unreferencedFiles.add(filePath);
for (const filePath of principal.entryPaths)
entryPaths.add(filePath);
principal.reconcileCache(graph);
if (isIsolateWorkspaces || (isSkipLibs && !isWatch)) {
factory.deletePrincipal(principal);
principals[i] = undefined;
}
perfObserver.addMemoryMark(factory.getPrincipalCount());
}
if (!isWatch && isSkipLibs && !isIsolateWorkspaces) {
for (const principal of principals) {
if (principal)
factory.deletePrincipal(principal);
}
principals.length = 0;
}
return {
graph,
entryPaths,
analyzedFiles,
unreferencedFiles,
analyzeSourceFile,
};
}