knip
Version:
Find and fix unused files, dependencies and exports in your TypeScript and JavaScript projects
276 lines (275 loc) • 13.7 kB
JavaScript
import { WorkspaceWorker } from '../WorkspaceWorker.js';
import { _getInputsFromScripts } from '../binaries/index.js';
import { getCompilerExtensions, getIncludedCompilers } from '../compilers/index.js';
import { debugLog, debugLogArray } from '../util/debug.js';
import { getReferencedInputsHandler } from '../util/get-referenced-inputs.js';
import { _glob, negate } from '../util/glob.js';
import { isConfig, isDeferResolveEntry, isDeferResolveProductionEntry, isEntry, isProductionEntry, 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 } from '../util/path.js';
import {} from '../util/tag.js';
import { augmentWorkspace, getToSourcePathHandler } 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 allConfigFilePaths = new Set();
const allConfigFilesMap = new Map();
const enabledPluginsStore = new Map();
const toSourceFilePath = getToSourcePathHandler(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 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) ?? []),
isCache,
cacheLocation,
allConfigFilePaths,
allConfigFilesMap,
});
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 = await getEntryPathsFromManifest(manifest, { ...sharedGlobOptions, ignore });
for (const id of entryPathsFromManifest.map(id => toProductionEntry(id)))
inputs.add(id);
const inputsFromPlugins = await worker.runPlugins();
for (const id of inputsFromPlugins)
inputs.add(id);
enabledPluginsStore.set(name, worker.enabledPlugins);
const principal = factory.createPrincipal({
cwd: dir,
paths: config.paths,
isFile,
compilerOptions,
compilers,
pkgName,
isIsolateWorkspaces,
isSkipLibs,
isWatch,
toSourceFilePath,
isCache,
cacheLocation,
});
const entryFilePatterns = new Set();
const productionEntryFilePatterns = new Set();
for (const input of inputs) {
const specifier = input.specifier;
if (isEntry(input)) {
entryFilePatterns.add(isAbsolute(specifier) ? relative(dir, specifier) : specifier);
}
else if (isProductionEntry(input)) {
productionEntryFilePatterns.add(isAbsolute(specifier) ? relative(dir, specifier) : specifier);
}
else if (!isConfig(input)) {
const ws = (input.containingFilePath && chief.findWorkspaceByFilePath(input.containingFilePath)) || workspace;
const resolvedFilePath = getReferencedInternalFilePath(input, ws);
if (resolvedFilePath) {
if (isDeferResolveProductionEntry(input)) {
productionEntryFilePatterns.add(resolvedFilePath);
}
else if (isDeferResolveEntry(input)) {
if (!isProduction || !input.optional)
entryFilePatterns.add(resolvedFilePath);
}
else {
principal.addEntryPath(resolvedFilePath, { skipExportsAnalysis: true });
}
}
}
}
if (isProduction) {
const negatedEntryPatterns = Array.from(entryFilePatterns).map(negate);
{
const label = 'entry';
const patterns = worker.getProductionEntryFilePatterns(negatedEntryPatterns);
const workspaceEntryPaths = await _glob({ ...sharedGlobOptions, patterns, gitignore: false, label });
principal.addEntryPaths(workspaceEntryPaths);
}
{
const label = 'production plugin entry';
const patterns = Array.from(productionEntryFilePatterns);
const pluginWorkspaceEntryPaths = await _glob({ ...sharedGlobOptions, patterns, label });
principal.addEntryPaths(pluginWorkspaceEntryPaths, { skipExportsAnalysis: true });
}
{
const label = 'project';
const patterns = worker.getProductionProjectFilePatterns(negatedEntryPatterns);
const workspaceProjectPaths = await _glob({ ...sharedGlobOptions, patterns, label });
for (const projectPath of workspaceProjectPaths)
principal.addProjectPath(projectPath);
}
}
else {
{
const label = 'entry';
const patterns = worker.getEntryFilePatterns();
const workspaceEntryPaths = await _glob({ ...sharedGlobOptions, patterns, gitignore: false, label });
principal.addEntryPaths(workspaceEntryPaths);
}
{
const label = 'project';
const patterns = worker.getProjectFilePatterns([...productionEntryFilePatterns]);
const workspaceProjectPaths = await _glob({ ...sharedGlobOptions, patterns, label });
for (const projectPath of workspaceProjectPaths)
principal.addProjectPath(projectPath);
}
{
const label = 'plugin entry';
const patterns = worker.getPluginEntryFilePatterns([...entryFilePatterns, ...productionEntryFilePatterns]);
const pluginWorkspaceEntryPaths = await _glob({ ...sharedGlobOptions, patterns, label });
principal.addEntryPaths(pluginWorkspaceEntryPaths, { skipExportsAnalysis: true });
}
{
const label = 'plugin project';
const patterns = worker.getPluginProjectFilePatterns();
const pluginWorkspaceProjectPaths = await _glob({ ...sharedGlobOptions, patterns, label });
for (const projectPath of pluginWorkspaceProjectPaths)
principal.addProjectPath(projectPath);
}
{
const label = 'plugin configuration';
const patterns = worker.getPluginConfigPatterns();
const configurationEntryPaths = await _glob({ ...sharedGlobOptions, patterns, label });
principal.addEntryPaths(configurationEntryPaths, { skipExportsAnalysis: true });
}
}
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();
streamer.cast('Running async compilers...');
await principal.runAsyncCompilers();
streamer.cast('Analyzing source files...');
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;
}
}
if (isIsolateWorkspaces) {
for (const principal of principals) {
if (principal)
factory.deletePrincipal(principal);
}
principals.length = 0;
}
return {
graph,
entryPaths,
analyzedFiles,
unreferencedFiles,
analyzeSourceFile,
};
}