knip
Version:
Find and fix unused dependencies, exports and files in your TypeScript and JavaScript projects
231 lines (230 loc) • 9.69 kB
JavaScript
import { extractSpecifiers } from "./typescript/follow-imports.js";
import { parseFile } from "./typescript/visitors/helpers.js";
import { CacheConsultant } from "./CacheConsultant.js";
import { getCompilerExtensions } from "./compilers/index.js";
import { DEFAULT_EXTENSIONS } from "./constants.js";
import { _getImportsAndExports } from "./typescript/get-imports-and-exports.js";
import { createBunShellVisitor } from "./typescript/visitors/script-visitors.js";
import { buildVisitor } from "./typescript/visitors/walk.js";
import { createCustomModuleResolver } from "./typescript/resolve-module-names.js";
import { SourceFileManager } from "./typescript/SourceFileManager.js";
import { compact } from "./util/array.js";
import { timerify } from "./util/Performance.js";
import { extname, isInNodeModules, toAbsolute } from "./util/path.js";
export class ProjectPrincipal {
entryPaths = new Set();
projectPaths = new Set();
programPaths = new Set();
skipExportsAnalysis = new Set();
pluginCtx = {
filePath: '',
sourceText: '',
addScript: () => { },
addImport: () => { },
};
pluginVisitorObjects = [];
_visitor;
syncCompilers = new Map();
asyncCompilers = new Map();
paths = {};
rootDirs = [];
extensions = new Set(DEFAULT_EXTENSIONS);
cache;
toSourceFilePath;
fileManager;
resolveModule = () => undefined;
resolvedFiles = new Set();
deletedFiles = new Set();
constructor(options, toSourceFilePath) {
this.cache = new CacheConsultant('root', options);
this.toSourceFilePath = toSourceFilePath;
this.pluginVisitorObjects.push(createBunShellVisitor(this.pluginCtx));
this.fileManager = new SourceFileManager({
compilers: [this.syncCompilers, this.asyncCompilers],
});
}
addCompilers(compilers) {
for (const [ext, compiler] of compilers[0]) {
if (!this.syncCompilers.has(ext)) {
this.syncCompilers.set(ext, compiler);
this.extensions.add(ext);
}
}
for (const [ext, compiler] of compilers[1]) {
if (!this.asyncCompilers.has(ext)) {
this.asyncCompilers.set(ext, compiler);
this.extensions.add(ext);
}
}
}
addPaths(paths, basePath) {
if (!paths)
return;
for (const key in paths) {
const prefixes = paths[key].map(prefix => toAbsolute(prefix, basePath));
if (key in this.paths) {
this.paths[key] = compact([...this.paths[key], ...prefixes]);
}
else {
this.paths[key] = prefixes;
}
}
}
addRootDirs(rootDirs) {
for (const dir of rootDirs) {
if (!this.rootDirs.includes(dir))
this.rootDirs.push(dir);
}
}
init() {
this.extensions = new Set([
...DEFAULT_EXTENSIONS,
...getCompilerExtensions([this.syncCompilers, this.asyncCompilers]),
]);
const customCompilerExtensions = getCompilerExtensions([this.syncCompilers, this.asyncCompilers]);
const pathsOrUndefined = Object.keys(this.paths).length > 0 ? this.paths : undefined;
const rootDirsOrUndefined = this.rootDirs.length > 1 ? this.rootDirs : undefined;
this.resolveModule = createCustomModuleResolver({ paths: pathsOrUndefined, rootDirs: rootDirsOrUndefined }, customCompilerExtensions, this.toSourceFilePath);
}
readFile(filePath) {
return this.fileManager.readFile(filePath);
}
hasAcceptedExtension(filePath) {
return this.extensions.has(extname(filePath));
}
addEntryPath(filePath, options) {
if (!isInNodeModules(filePath) && this.hasAcceptedExtension(filePath)) {
this.entryPaths.add(filePath);
this.projectPaths.add(filePath);
if (options?.skipExportsAnalysis)
this.skipExportsAnalysis.add(filePath);
}
}
addEntryPaths(filePaths, options) {
for (const filePath of filePaths)
this.addEntryPath(filePath, options);
}
addProgramPath(filePath) {
if (!isInNodeModules(filePath) && this.hasAcceptedExtension(filePath)) {
this.programPaths.add(filePath);
}
}
addProjectPath(filePath) {
if (!isInNodeModules(filePath) && this.hasAcceptedExtension(filePath)) {
this.projectPaths.add(filePath);
this.deletedFiles.delete(filePath);
}
}
removeProjectPath(filePath) {
this.entryPaths.delete(filePath);
this.projectPaths.delete(filePath);
this.invalidateFile(filePath);
this.deletedFiles.add(filePath);
}
async runAsyncCompilers() {
const add = timerify(this.fileManager.compileAndAddSourceFile.bind(this.fileManager));
const extensions = Array.from(this.asyncCompilers.keys());
const files = Array.from(this.projectPaths).filter(filePath => extensions.includes(extname(filePath)));
for (const filePath of files) {
await add(filePath);
}
}
walkAndAnalyze(analyzeFile) {
this.resolvedFiles.clear();
const visited = new Set([...this.entryPaths, ...this.programPaths]);
let lastEntrySize = this.entryPaths.size;
let lastProgramSize = this.programPaths.size;
for (const filePath of visited) {
const sourceText = this.fileManager.readFile(filePath);
if (!sourceText) {
if (this.projectPaths.has(filePath))
analyzeFile(filePath, undefined, '');
continue;
}
try {
const result = parseFile(filePath, sourceText);
this.fileManager.sourceTextCache.delete(filePath);
if (this.projectPaths.has(filePath)) {
const internalPaths = analyzeFile(filePath, result, sourceText);
if (internalPaths) {
for (const p of internalPaths)
visited.add(p);
}
}
else {
for (const specifier of extractSpecifiers(result, sourceText, filePath)) {
const resolved = this.resolveSpecifier(specifier, filePath);
if (resolved && !isInNodeModules(resolved))
visited.add(resolved);
}
}
if (this.entryPaths.size > lastEntrySize || this.programPaths.size > lastProgramSize) {
for (const p of this.entryPaths)
visited.add(p);
for (const p of this.programPaths)
visited.add(p);
lastEntrySize = this.entryPaths.size;
lastProgramSize = this.programPaths.size;
}
}
catch {
}
}
this.resolvedFiles = visited;
}
getUsedResolvedFiles() {
this.resolvedFiles.clear();
const visited = new Set([...this.entryPaths, ...this.programPaths]);
for (const filePath of visited) {
const sourceText = this.fileManager.readFile(filePath);
if (!sourceText)
continue;
try {
const result = parseFile(filePath, sourceText);
for (const specifier of extractSpecifiers(result, sourceText, filePath)) {
const resolved = this.resolveSpecifier(specifier, filePath);
if (resolved && !isInNodeModules(resolved))
visited.add(resolved);
}
}
catch {
}
}
this.resolvedFiles = visited;
return Array.from(this.projectPaths).filter(filePath => visited.has(filePath));
}
resolveSpecifier(specifier, containingFile) {
return this.resolveModule(specifier, containingFile)?.resolvedFileName;
}
getUnreferencedFiles() {
return Array.from(this.projectPaths).filter(filePath => !this.resolvedFiles.has(filePath));
}
analyzeSourceFile(filePath, options, ignoreExportsUsedInFile, parseResult, sourceText) {
const fd = this.cache.getFileDescriptor(filePath);
if (!fd.changed && fd.meta?.data)
return fd.meta.data;
sourceText ??= this.fileManager.readFile(filePath);
const skipExports = this.skipExportsAnalysis.has(filePath);
if (options.isFixExports || options.isFixTypes) {
const ext = extname(filePath);
if (!DEFAULT_EXTENSIONS.has(ext) && (this.syncCompilers.has(ext) || this.asyncCompilers.has(ext))) {
options = { ...options, isFixExports: false, isFixTypes: false };
}
}
if (!this._visitor)
this._visitor = buildVisitor(this.pluginVisitorObjects, !!ignoreExportsUsedInFile);
return _getImportsAndExports(filePath, sourceText, this.resolveModule, options, ignoreExportsUsedInFile, skipExports, this._visitor, this.pluginVisitorObjects.length > 0 ? this.pluginCtx : undefined, parseResult);
}
invalidateFile(filePath) {
this.fileManager.invalidate(filePath);
}
reconcileCache(graph) {
for (const [filePath, file] of graph) {
const fd = this.cache.getFileDescriptor(filePath);
if (!fd?.meta)
continue;
fd.meta.data = { ...file, internalImportCache: undefined, importedBy: undefined };
}
this.cache.reconcile();
}
}