knip
Version:
Find and fix unused dependencies, exports and files in your TypeScript and JavaScript projects
117 lines (116 loc) • 4.86 kB
JavaScript
import { parseTsconfig } from 'get-tsconfig';
import { isFile } from './fs.js';
import { _syncGlob } from './glob.js';
import { dirname, isAbsolute, join, toAbsolute } from './path.js';
const hasGlobChar = (p) => p.includes('*') || p.includes('?');
const hasExtension = (p) => {
const last = p.lastIndexOf('/');
const base = last >= 0 ? p.slice(last + 1) : p;
return base !== '.' && base !== '..' && base.includes('.');
};
const resolvePatterns = (patterns, dir, expandDirs = false) => {
if (!patterns)
return undefined;
return patterns.map(p => {
const resolved = isAbsolute(p) ? p : join(dir, p);
return expandDirs && !hasGlobChar(p) && !hasExtension(p) ? join(resolved, '**/*') : resolved;
});
};
const DEFAULT_INCLUDE = ['**/*'];
const TS_EXTENSIONS = new Set(['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs']);
const isDtsExt = /\.d\.(m|c)?ts$/;
const isTsRelevant = (filePath) => {
if (isDtsExt.test(filePath))
return true;
const ext = filePath.slice(filePath.lastIndexOf('.'));
return TS_EXTENSIONS.has(ext);
};
const expandFileNames = (dir, compilerOptions, include, exclude, files) => {
const result = [];
if (files) {
for (const file of files)
result.push(file);
}
const effectiveExclude = [...(exclude ?? []), join(dir, 'node_modules/**')];
if (compilerOptions.outDir) {
effectiveExclude.push(join(compilerOptions.outDir, '**'));
}
const effectiveInclude = include ?? (files ? undefined : DEFAULT_INCLUDE.map(p => join(dir, p)));
if (effectiveInclude) {
const negated = effectiveExclude.map(p => `!${p}`);
const globbed = _syncGlob({ patterns: [...effectiveInclude, ...negated], cwd: dir });
for (const f of globbed)
if (isTsRelevant(f))
result.push(f);
}
return result;
};
const resolveReference = (refPath, dir) => {
const abs = isAbsolute(refPath) ? refPath : join(dir, refPath);
if (isFile(abs))
return abs;
const withTsconfig = join(abs, 'tsconfig.json');
return isFile(withTsconfig) ? withTsconfig : undefined;
};
const absDir = (path, dir) => toAbsolute(path, dir).replace(/\/+$/, '');
const walkReferences = (target, references, dir, visited, pairs) => {
if (!references?.length)
return;
for (const ref of references) {
const refPath = resolveReference(ref.path, dir);
if (!refPath || visited.has(refPath))
continue;
visited.add(refPath);
const refConfig = parseTsconfig(refPath);
const refDir = dirname(refPath);
const refOpts = refConfig.compilerOptions;
const refOutDir = refOpts?.outDir ? absDir(refOpts.outDir, refDir) : undefined;
const refRootDir = refOpts?.rootDir ? absDir(refOpts.rootDir, refDir) : undefined;
if (refOutDir && refRootDir && refOutDir !== refRootDir)
pairs.push({ srcDir: refRootDir, outDir: refOutDir });
if (refOutDir && !target.outDir)
target.outDir = refOutDir;
if (refRootDir && !target.rootDir)
target.rootDir = refRootDir;
if (!refOutDir || !refRootDir)
walkReferences(target, refConfig.references, refDir, visited, pairs);
}
};
const EMPTY = {
compilerOptions: {},
fileNames: [],
include: undefined,
exclude: undefined,
sourceMapPairs: [],
};
export const loadTSConfig = async (tsConfigFilePath) => {
if (!isFile(tsConfigFilePath))
return { isFile: false, ...EMPTY };
try {
const config = parseTsconfig(tsConfigFilePath);
const dir = dirname(tsConfigFilePath);
const compilerOptions = (config.compilerOptions ?? {});
if (compilerOptions.outDir)
compilerOptions.outDir = absDir(compilerOptions.outDir, dir);
if (compilerOptions.rootDir)
compilerOptions.rootDir = absDir(compilerOptions.rootDir, dir);
if (compilerOptions.paths) {
compilerOptions.pathsBasePath ??= dir;
}
if (compilerOptions.rootDirs) {
compilerOptions.rootDirs = compilerOptions.rootDirs.map((d) => (isAbsolute(d) ? d : join(dir, d)));
}
const sourceMapPairs = [];
if (config.references?.length) {
walkReferences(compilerOptions, config.references, dir, new Set([tsConfigFilePath]), sourceMapPairs);
}
const include = resolvePatterns(config.include, dir, true);
const exclude = resolvePatterns(config.exclude, dir, true);
const files = resolvePatterns(config.files, dir);
const fileNames = expandFileNames(dir, compilerOptions, include, exclude, files);
return { isFile: true, compilerOptions, fileNames, include, exclude, sourceMapPairs };
}
catch {
return { isFile: true, ...EMPTY };
}
};