@adguard/aglint
Version:
Universal adblock filter list linter.
86 lines (83 loc) • 3.6 kB
JavaScript
/*
* AGLint v3.0.0 (build date: Wed, 21 May 2025 13:24:14 GMT)
* (c) 2025 AdGuard Software Ltd.
* Released under the MIT license
* https://github.com/AdguardTeam/AGLint#readme
*/
import { readdir, readFile, stat } from 'node:fs/promises';
import path from 'node:path';
import ignore from 'ignore';
import { PROBLEMATIC_PATHS, IGNORE_FILE_NAME, CONFIG_FILE_NAMES, SUPPORTED_EXTENSIONS } from './constants.js';
/**
* Ignore instance for the default ignores
*/
const defaultIgnores = ignore().add(PROBLEMATIC_PATHS);
/**
* Searches for lintable files in a directory recursively. Practically it means files with the
* supported extensions. It will also search for config files.
*
* It will ignore files and directories that are ignored by the ignore file at any level.
* `.aglintignore` works exactly like `.gitignore`.
*
* @param dir Directory to search in
* @param ignores File ignores
* @param useIgnoreFiles Use ignore files or not (default: true)
* @returns The result of the scan (`ScannedDirectory` object)
* @throws If multiple config files are found in the given directory
*/
async function scan(dir, ignores = [], useIgnoreFiles = true) {
// Initialize an empty result
const result = {
dir: path.parse(path.join(dir)),
configFile: null,
lintableFiles: [],
subdirectories: [],
};
// Get all files in the directory
const items = await readdir(dir);
// First of all, let's parse the ignore file in the current directory if ignore files are
// enabled and the ignore file exists
if (useIgnoreFiles && items.includes(IGNORE_FILE_NAME)) {
const content = await readFile(path.join(dir, IGNORE_FILE_NAME), 'utf8');
ignores.push(ignore().add(content));
}
// Loop through all items in the directory
for (const item of items) {
// If the current item is ignored by the default ignores, skip it
if (defaultIgnores.ignores(item)) {
continue;
}
// If the current item is ignored by the ignore files, skip it (if ignore files are enabled)
if (useIgnoreFiles && ignores.some((i) => i.ignores(item))) {
continue;
}
// Get the full path of the current item within the directory structure,
// then get the stats of the item
const itemPath = path.join(dir, item);
const stats = await stat(itemPath);
if (stats.isFile()) {
// Parse path
const parsedPath = path.parse(itemPath);
// If the file is a config file
if (CONFIG_FILE_NAMES.has(item)) {
// If a config file is already found, throw an error
if (result.configFile !== null) {
throw new Error(`Multiple config files found in the same directory: "${path.join(result.configFile.dir, result.configFile.base)}" and "${itemPath}"`);
}
// Otherwise, set the config file for the current directory
result.configFile = parsedPath;
}
else if (SUPPORTED_EXTENSIONS.has(parsedPath.ext)) {
// We only want to lint files with the supported extensions
result.lintableFiles.push(parsedPath);
}
}
else if (stats.isDirectory()) {
// If the current item is a directory, recursively scan it, then
// merge the subdirectory result into the current result
result.subdirectories.push(await scan(itemPath, ignores, useIgnoreFiles));
}
}
return result;
}
export { scan };