knip
Version:
Find and fix unused dependencies, exports and files in your TypeScript and JavaScript projects
177 lines (176 loc) • 7.29 kB
JavaScript
import { readFileSync } from 'node:fs';
import { promisify } from 'node:util';
import { walk as _walk } from '@nodelib/fs.walk';
import fg, {} from 'fast-glob';
import picomatch from 'picomatch';
import { GLOBAL_IGNORE_PATTERNS, ROOT_WORKSPACE_NAME } from '../constants.js';
import { timerify } from './Performance.js';
import { compact } from './array.js';
import { debugLogObject } from './debug.js';
import { isDirectory, isFile } from './fs.js';
import { parseAndConvertGitignorePatterns } from './parse-and-convert-gitignores.js';
import { dirname, join, relative, toPosix } from './path.js';
const walk = promisify(_walk);
const _picomatch = timerify(picomatch);
const cachedGitIgnores = new Map();
const cachedGlobIgnores = new Map();
const findAncestorGitignoreFiles = (cwd) => {
const gitignorePaths = [];
if (isDirectory(join(cwd, '.git')))
return gitignorePaths;
let dir = dirname(cwd);
let prev;
while (dir) {
const filePath = join(dir, '.gitignore');
if (isFile(filePath))
gitignorePaths.push(filePath);
if (isDirectory(join(dir, '.git')))
break;
dir = dirname((prev = dir));
if (prev === dir || dir === '.')
break;
}
return gitignorePaths;
};
export const findAndParseGitignores = async (cwd) => {
const init = ['.git', ...GLOBAL_IGNORE_PATTERNS];
const ignores = new Set(init);
const unignores = [];
const gitignoreFiles = [];
const pmOptions = { ignore: unignores };
const matchers = new Set(init.map(pattern => _picomatch(pattern, pmOptions)));
const matcher = (str) => {
for (const isMatch of matchers) {
const state = isMatch(str);
if (state)
return state;
}
return false;
};
const addFile = (filePath, baseDir) => {
gitignoreFiles.push(relative(cwd, filePath));
const dir = baseDir ?? dirname(toPosix(filePath));
const base = relative(cwd, dir);
const ancestor = base.startsWith('..') ? `${relative(dir, cwd)}/` : undefined;
const ignoresForDir = new Set(base === '' ? init : []);
const unignoresForDir = new Set();
const patterns = readFileSync(filePath, 'utf8');
for (const rule of parseAndConvertGitignorePatterns(patterns, ancestor)) {
const [pattern, extraPattern] = rule.patterns;
if (rule.negated) {
if (base === '' || base.startsWith('..')) {
if (!unignores.includes(extraPattern)) {
unignores.push(...rule.patterns);
unignoresForDir.add(pattern);
unignoresForDir.add(extraPattern);
}
}
else {
if (!unignores.includes(extraPattern.startsWith('**/') ? extraPattern : `**/${extraPattern}`)) {
const unignore = join(base, pattern);
const extraUnignore = join(base, extraPattern);
unignores.push(unignore, extraUnignore);
unignoresForDir.add(unignore);
unignoresForDir.add(extraUnignore);
}
}
}
else {
if (base === '' || base.startsWith('..')) {
ignores.add(pattern);
ignores.add(extraPattern);
ignoresForDir.add(pattern);
ignoresForDir.add(extraPattern);
}
else if (!unignores.includes(extraPattern.startsWith('**/') ? extraPattern : `**/${extraPattern}`)) {
const ignore = join(base, pattern);
const extraIgnore = join(base, extraPattern);
ignores.add(ignore);
ignores.add(extraIgnore);
ignoresForDir.add(ignore);
ignoresForDir.add(extraIgnore);
}
}
}
const cacheDir = ancestor ? cwd : dir;
const cacheForDir = cachedGitIgnores.get(cacheDir);
if (cacheForDir) {
for (const pattern of ignoresForDir)
cacheForDir.ignores.add(pattern);
for (const pattern of unignoresForDir)
cacheForDir.unignores.add(pattern);
}
else {
cachedGitIgnores.set(cacheDir, { ignores: ignoresForDir, unignores: unignoresForDir });
}
for (const pattern of ignoresForDir)
matchers.add(_picomatch(pattern, pmOptions));
};
for (const filePath of findAncestorGitignoreFiles(cwd))
addFile(filePath);
if (isFile('.git/info/exclude'))
addFile('.git/info/exclude', cwd);
const entryFilter = (entry) => {
if (entry.dirent.isFile() && entry.name === '.gitignore') {
addFile(entry.path);
return true;
}
return false;
};
const deepFilter = (entry) => !matcher(relative(cwd, entry.path));
await walk(cwd, {
entryFilter: timerify(entryFilter),
deepFilter: timerify(deepFilter),
});
debugLogObject('*', 'Parsed gitignore files', { gitignoreFiles });
return { gitignoreFiles, ignores, unignores };
};
const _parseFindGitignores = timerify(findAndParseGitignores);
export async function glob(patterns, options) {
if (Array.isArray(patterns) && patterns.length === 0)
return [];
const hasCache = cachedGlobIgnores.has(options.dir);
const willCache = !hasCache && options.gitignore && options.label;
const cachedIgnores = options.gitignore ? cachedGlobIgnores.get(options.dir) : undefined;
const _ignore = options.gitignore && Array.isArray(options.ignore) ? [...options.ignore] : [];
if (options.gitignore) {
if (willCache) {
let dir = options.dir;
let prev;
while (dir) {
const cacheForDir = cachedGitIgnores.get(dir);
if (cacheForDir) {
_ignore.push(...cacheForDir.ignores);
}
dir = dirname((prev = dir));
if (prev === dir || dir === '.')
break;
}
}
}
else {
_ignore.push(...GLOBAL_IGNORE_PATTERNS);
}
const ignore = cachedIgnores || compact(_ignore);
if (willCache)
cachedGlobIgnores.set(options.dir, compact(_ignore));
const { dir, label, ...fgOptions } = { ...options, ignore };
const paths = await fg.glob(patterns, fgOptions);
const name = relative(options.cwd, dir) || ROOT_WORKSPACE_NAME;
debugLogObject(name, label ? `Finding ${label}` : 'Finding paths', () => ({
patterns,
...fgOptions,
ignore: hasCache ? `// using cache from ${name}` : ignore,
paths,
}));
return paths;
}
export async function getGitIgnoredHandler(options) {
cachedGitIgnores.clear();
if (options.gitignore === false)
return () => false;
const { ignores, unignores } = await _parseFindGitignores(options.cwd);
const matcher = _picomatch(Array.from(ignores), { ignore: unignores });
const isGitIgnored = (filePath) => matcher(relative(options.cwd, filePath));
return timerify(isGitIgnored);
}