UNPKG

textlint

Version:

The pluggable linting tool for natural language.

174 lines (167 loc) 5.29 kB
import fs from "node:fs/promises"; import debug0 from "debug"; import path from "node:path"; import { glob } from "glob"; import { pathToGlobPattern } from "path-to-glob-pattern"; const debug = debug0("textlint:find-util"); const DEFAULT_IGNORE_PATTERNS = ["**/.git/**", "**/node_modules/**"]; /** * Convert file paths to glob patterns * @param patterns file paths or patterns * @param options */ export function pathsToGlobPatterns( patterns: string[], options: { extensions?: string[]; cwd?: string } = {} ): string[] { const processPatterns = pathToGlobPattern({ extensions: options.extensions || [], cwd: options.cwd || process.cwd() }); return patterns.map(processPatterns); } export type SearchFilesOptions = { cwd: string; ignoreFilePath?: string; }; export type SearchFilesNoTargetFileError = { type: "SearchFilesNoTargetFileError"; }; export type SearchFilesResultError = SearchFilesNoTargetFileError; export type SearchFilesResult = | { ok: true; items: string[]; } | { ok: false; errors: SearchFilesResultError[]; }; const createIgnorePatterns = async (cwd: string, ignoreFilePath: string): Promise<string[]> => { try { const normalizeIgnoreFilePath = path.resolve(cwd, ignoreFilePath); const ignoreFileContent = await fs.readFile(normalizeIgnoreFilePath, "utf-8"); return ignoreFileContent.split(/\r?\n/).filter((line: string) => !/^\s*$/.test(line) && !/^\s*#/.test(line)); } catch (error) { // If ignore file doesn't exist, return empty array (for default .textlintignore behavior) if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") { debug("ignore file not found: %s", ignoreFilePath); return []; } throw new Error(`Failed to read ignore file: ${ignoreFilePath}`, { cause: error }); } }; /** * globby wrapper that support ignore options * @param patterns * @param options */ export const searchFiles = async (patterns: string[], options: SearchFilesOptions): Promise<SearchFilesResult> => { const cwd = options.cwd ?? process.cwd(); const ignoredPatterns: string[] = [...DEFAULT_IGNORE_PATTERNS]; // Only add ignore patterns if ignoreFilePath is explicitly provided if (options.ignoreFilePath) { const ignorePatterns = await createIgnorePatterns(cwd, options.ignoreFilePath); ignoredPatterns.push(...ignorePatterns); } debug("search patterns: %o", patterns); debug("search ignore patterns: %o", ignoredPatterns); // Glob support file path, we can pass file path directly // https://github.com/azu/node-glob-example // TODO: add pathsToGlobPatterns here const files = await glob(patterns, { cwd, absolute: true, nodir: true, dot: true, ignore: ignoredPatterns }); debug("found files: %o", files); if (files.length > 0) { return { ok: true, items: files }; } // If ignore file is matched and result is empty, it should be ignored const filesWithoutIgnoreFiles = await glob(patterns, { cwd, absolute: true, nodir: true, dot: true // no ignore }); const isEmptyResultByIgnoreFile = files.length === 0 && filesWithoutIgnoreFiles.length !== 0; if (isEmptyResultByIgnoreFile) { debug("all files are ignored by ignore files. ignored files: %o", filesWithoutIgnoreFiles); return { ok: true, items: [] }; } // Not found target file debug("Not found target file"); return { ok: false, errors: [ { type: "SearchFilesNoTargetFileError" } ] }; }; export type ScanFilePathNoExistFilePathError = { type: "ScanFilePathNoExistFilePathError"; filePath: string; }; export type ScanFilePathResultError = ScanFilePathNoExistFilePathError | SearchFilesResultError; export type ScanFilePathResult = | { // Found target file status: "ok"; } | { // Found target file but it is ignored by ignore file status: "ignored"; } | { // Not found target file status: "error"; errors: ScanFilePathResultError[]; }; /** * Scan file path and return the file is target or not * @param filePath * @param options */ export const scanFilePath = async (filePath: string, options: SearchFilesOptions): Promise<ScanFilePathResult> => { const exists = await fs.stat(filePath).catch(() => null); if (!exists) { return { status: "error", errors: [ { type: "ScanFilePathNoExistFilePathError", filePath } ] }; } const searchResult = await searchFiles([filePath], options); if (!searchResult.ok) { return { status: "error", errors: searchResult.errors }; } if (searchResult.items.length === 0) { return { status: "ignored" }; } return { status: "ok" }; };