UNPKG

globby

Version:
869 lines (704 loc) 26.9 kB
import process from 'node:process'; import fs from 'node:fs'; import fsPromises from 'node:fs/promises'; import path from 'node:path'; import os from 'node:os'; import fastGlob from 'fast-glob'; import gitIgnore from 'ignore'; import isPathInside from 'is-path-inside'; import slash from 'slash'; import {toPath} from 'unicorn-magic/node'; import { isNegativePattern, bindFsMethod, promisifyFsMethod, findGitRoot, findGitRootSync, getParentGitignorePaths, } from './utilities.js'; const defaultIgnoredDirectories = [ '**/node_modules', '**/flow-typed', '**/coverage', '**/.git', ]; const ignoreFilesGlobOptions = { absolute: true, dot: true, }; export const GITIGNORE_FILES_PATTERN = '**/.gitignore'; // Maximum depth for [include] chains to prevent stack overflow (git uses 10) const MAX_INCLUDE_DEPTH = 10; const getReadFileMethod = fsImplementation => bindFsMethod(fsImplementation?.promises, 'readFile') ?? bindFsMethod(fsPromises, 'readFile') ?? promisifyFsMethod(fsImplementation, 'readFile'); const getReadFileSyncMethod = fsImplementation => bindFsMethod(fsImplementation, 'readFileSync') ?? bindFsMethod(fs, 'readFileSync'); const shouldSkipIgnoreFileError = (error, suppressErrors) => { if (!error) { return Boolean(suppressErrors); } if (error.code === 'ENOENT' || error.code === 'ENOTDIR') { return true; } return Boolean(suppressErrors); }; const createReadError = (kind, filePath, error) => { const prefix = `Failed to read ${kind} at ${filePath}`; if (error instanceof Error) { return new Error(`${prefix}: ${error.message}`, {cause: error}); } return new Error(`${prefix}: ${String(error)}`); }; const createIgnoreFileReadError = (filePath, error) => createReadError('ignore file', filePath, error); const createGitConfigReadError = (filePath, error) => createReadError('git config', filePath, error); const processIgnoreFileCore = (filePath, readMethod, suppressErrors) => { try { const content = readMethod(filePath, 'utf8'); return {filePath, content}; } catch (error) { if (shouldSkipIgnoreFileError(error, suppressErrors)) { return undefined; } throw createIgnoreFileReadError(filePath, error); } }; const readIgnoreFilesSafely = async (paths, readFileMethod, suppressErrors) => { const fileResults = await Promise.all(paths.map(async filePath => { try { const content = await readFileMethod(filePath, 'utf8'); return {filePath, content}; } catch (error) { if (shouldSkipIgnoreFileError(error, suppressErrors)) { return undefined; } throw createIgnoreFileReadError(filePath, error); } })); return fileResults.filter(Boolean); }; const readIgnoreFilesSafelySync = (paths, readFileSyncMethod, suppressErrors) => paths .map(filePath => processIgnoreFileCore(filePath, readFileSyncMethod, suppressErrors)) .filter(Boolean); const dedupePaths = paths => { const seen = new Set(); return paths.filter(filePath => { if (seen.has(filePath)) { return false; } seen.add(filePath); return true; }); }; const globIgnoreFiles = (globFunction, patterns, normalizedOptions) => globFunction(patterns, { ...normalizedOptions, ...ignoreFilesGlobOptions, // Must be last to ensure absolute/dot flags stick }); const getParentIgnorePaths = (gitRoot, normalizedOptions) => gitRoot ? getParentGitignorePaths(gitRoot, normalizedOptions.cwd) : []; const combineIgnoreFilePaths = (gitRoot, normalizedOptions, childPaths) => dedupePaths([ ...getParentIgnorePaths(gitRoot, normalizedOptions), ...childPaths, ]); const buildIgnoreResult = (files, normalizedOptions, gitRoot) => { const baseDir = gitRoot || normalizedOptions.cwd; const patterns = getPatternsFromIgnoreFiles(files, baseDir); const matcher = createIgnoreMatcher(patterns, normalizedOptions.cwd, baseDir); return { patterns, matcher, predicate: fileOrDirectory => matcher(fileOrDirectory).ignored, usingGitRoot: Boolean(gitRoot && gitRoot !== normalizedOptions.cwd), }; }; // Apply base path to gitignore patterns based on .gitignore spec 2.22.1 // https://git-scm.com/docs/gitignore#_pattern_format // See also https://github.com/sindresorhus/globby/issues/146 const applyBaseToPattern = (pattern, base) => { if (!base) { return pattern; } const isNegative = isNegativePattern(pattern); const cleanPattern = isNegative ? pattern.slice(1) : pattern; // Check if pattern has non-trailing slashes const slashIndex = cleanPattern.indexOf('/'); const hasNonTrailingSlash = slashIndex !== -1 && slashIndex !== cleanPattern.length - 1; let result; if (!hasNonTrailingSlash) { // "If there is no separator at the beginning or middle of the pattern, // then the pattern may also match at any level below the .gitignore level." // So patterns like '*.log' or 'temp' or 'build/' (trailing slash) match recursively. result = path.posix.join(base, '**', cleanPattern); } else if (cleanPattern.startsWith('/')) { // "If there is a separator at the beginning [...] of the pattern, // then the pattern is relative to the directory level of the particular .gitignore file itself." // Leading slash anchors the pattern to the .gitignore's directory. result = path.posix.join(base, cleanPattern.slice(1)); } else { // "If there is a separator [...] middle [...] of the pattern, // then the pattern is relative to the directory level of the particular .gitignore file itself." // Patterns like 'src/foo' are relative to the .gitignore's directory. result = path.posix.join(base, cleanPattern); } return isNegative ? '!' + result : result; }; const parseIgnoreFile = (file, cwd) => { const base = slash(path.relative(cwd, path.dirname(file.filePath))); return file.content .split(/\r?\n/) .filter(line => line && !line.startsWith('#')) .map(pattern => applyBaseToPattern(pattern, base)); }; const toRelativePath = (fileOrDirectory, cwd) => { if (path.isAbsolute(fileOrDirectory)) { // When paths are equal, path.relative returns empty string which is valid // isPathInside returns false for equal paths, so check this case first const relativePath = path.relative(cwd, fileOrDirectory); if (relativePath && !isPathInside(fileOrDirectory, cwd)) { // Path is outside cwd - it cannot be ignored by patterns in cwd // Return undefined to indicate this path is outside scope return undefined; } return relativePath; } // Normalize relative paths: // - Git treats './foo' as 'foo' when checking against patterns // - Patterns starting with './' in .gitignore are invalid and don't match anything // - The ignore library expects normalized paths without './' prefix if (fileOrDirectory.startsWith('./')) { return fileOrDirectory.slice(2); } // Paths with ../ point outside cwd and cannot match patterns from this directory // Return undefined to indicate this path is outside scope if (fileOrDirectory.startsWith('../')) { return undefined; } return fileOrDirectory; }; const notIgnored = {ignored: false, unignored: false}; const createIgnoreMatcher = (patterns, cwd, baseDir) => { const ignores = gitIgnore().add(patterns); // Normalize to handle path separator and . / .. components consistently const resolvedCwd = path.normalize(path.resolve(cwd)); const resolvedBaseDir = path.normalize(path.resolve(baseDir)); return fileOrDirectory => { fileOrDirectory = toPath(fileOrDirectory); const hasTrailingSeparator = /[/\\]$/.test(fileOrDirectory); // Never ignore the cwd itself - use normalized comparison const normalizedPath = path.normalize(path.resolve(fileOrDirectory)); if (normalizedPath === resolvedCwd) { return notIgnored; } // Convert to relative path from baseDir (use normalized baseDir) let relativePath = toRelativePath(fileOrDirectory, resolvedBaseDir); // If path is outside baseDir (undefined), it can't be ignored by patterns if (relativePath === undefined) { return notIgnored; } if (!relativePath) { return notIgnored; } if (hasTrailingSeparator && !relativePath.endsWith(path.sep)) { relativePath += path.sep; } return ignores.test(slash(relativePath)); }; }; const normalizeOptions = (options = {}) => { const ignoreOption = options.ignore ? (Array.isArray(options.ignore) ? options.ignore : [options.ignore]) : []; const cwd = toPath(options.cwd) ?? process.cwd(); // Adjust deep option for fast-glob: fast-glob's deep counts differently than expected // User's deep: 0 = root only -> fast-glob needs: 1 // User's deep: 1 = root + 1 level -> fast-glob needs: 2 const deep = typeof options.deep === 'number' ? Math.max(0, options.deep) + 1 : Number.POSITIVE_INFINITY; // Only pass through specific fast-glob options that make sense for finding ignore files return { cwd, suppressErrors: options.suppressErrors ?? false, deep, ignore: [...ignoreOption, ...defaultIgnoredDirectories], followSymbolicLinks: options.followSymbolicLinks ?? true, concurrency: options.concurrency, throwErrorOnBrokenSymbolicLink: options.throwErrorOnBrokenSymbolicLink ?? false, fs: options.fs, }; }; const unescapeGitQuotedValue = value => value.replaceAll(/\\(["\\abfnrtv])/g, (_match, escapedCharacter) => { switch (escapedCharacter) { case 'a': { return '\u0007'; } case 'b': { return '\b'; } case 'f': { return '\f'; } case 'n': { return '\n'; } case 'r': { return '\r'; } case 't': { return '\t'; } case 'v': { return '\v'; } default: { return escapedCharacter; } } }); const parseGitConfigValue = value => { const trimmedValue = value.trim(); const quotedMatch = trimmedValue.match(/^"((?:[^"\\]|\\.)*)"\s*(?:[#;].*)?$/); if (quotedMatch) { return unescapeGitQuotedValue(quotedMatch[1]); } return trimmedValue.replace(/\s[#;].*$/, '').trim(); }; const resolveConfigPath = (filePath, configPath) => { if (configPath.startsWith('~/')) { const homeDirectory = os.homedir(); const resolved = path.join(homeDirectory, configPath.slice(2)); // Ensure the resolved path is within the home directory to prevent traversal via ~/.. if (!isPathInside(resolved, homeDirectory)) { // Invalid path, return a path that won't exist return path.join(homeDirectory, '.globby-invalid-path-traversal'); } return resolved; } if (path.isAbsolute(configPath)) { return configPath; } return path.resolve(path.dirname(filePath), configPath); }; const parseGitConfigSection = line => { if (!line.startsWith('[')) { return undefined; } let inQuotes = false; let isEscaped = false; for (let index = 1; index < line.length; index++) { const character = line[index]; if (isEscaped) { isEscaped = false; continue; } if (character === '\\') { isEscaped = true; continue; } if (character === '"') { inQuotes = !inQuotes; continue; } if (character === ']' && !inQuotes) { const remainder = line.slice(index + 1).trimStart(); if (remainder && !remainder.startsWith('#') && !remainder.startsWith(';')) { return undefined; } return line.slice(1, index).trim(); } } return undefined; }; const parseGitConfigEntry = line => { const match = line.match(/^([A-Za-z\d-.]+)\s*=\s*(.*)$/); if (!match) { return undefined; } return { key: match[1].toLowerCase(), value: parseGitConfigValue(match[2]), }; }; const parseIncludeIfCondition = section => { if (!section) { return undefined; } const match = section.match(/^includeif\s+"([^"]+)"$/i); return match ? match[1] : undefined; }; const normalizeGitConfigConditionPattern = (pattern, configFilePath) => { if (pattern.startsWith('~/')) { pattern = path.join(os.homedir(), pattern.slice(2)); } else if (pattern.startsWith('./')) { pattern = path.resolve(path.dirname(configFilePath), pattern.slice(2)); } else if (!path.isAbsolute(pattern)) { pattern = `**/${pattern}`; } if (pattern.endsWith('/')) { pattern += '**'; } return slash(pattern); }; const gitConfigGlobToRegex = (pattern, flags) => { let regex = ''; for (let index = 0; index < pattern.length; index++) { const character = pattern[index]; const nextCharacter = pattern[index + 1]; const nextNextCharacter = pattern[index + 2]; if (character === '*' && nextCharacter === '*' && nextNextCharacter === '/') { regex += '(?:.*/)?'; index += 2; continue; } if (character === '*' && nextCharacter === '*') { regex += '.*'; index += 1; continue; } if (character === '*') { regex += '[^/]*'; continue; } if (character === '?') { regex += '[^/]'; continue; } if (character === '[') { const closingBracketIndex = pattern.indexOf(']', index + 1); if (closingBracketIndex !== -1) { const bracketContent = pattern.slice(index + 1, closingBracketIndex); if (bracketContent) { const negatedBracketContent = bracketContent[0] === '!' ? `^${bracketContent.slice(1)}` : bracketContent; regex += `[${negatedBracketContent}]`; index = closingBracketIndex; continue; } } } regex += /[|\\{}()[\]^$+?.]/.test(character) ? `\\${character}` : character; } try { return new RegExp(`^${regex}$`, flags); } catch { // If regex construction fails (e.g., invalid bracket expression), return a non-matching pattern return /(?!)/; } }; const matchesIncludeIfCondition = (condition, gitDirectory, configFilePath) => { if (!gitDirectory) { return false; } const match = condition.match(/^(gitdir|gitdir\/i):(.*)$/i); if (!match) { return false; } const [, keyword, rawPattern] = match; const pattern = normalizeGitConfigConditionPattern(rawPattern.trim(), configFilePath); const isCaseInsensitive = keyword.toLowerCase() === 'gitdir/i'; const regularExpression = gitConfigGlobToRegex(pattern, isCaseInsensitive ? 'i' : undefined); const normalizedGitDirectory = slash(path.resolve(gitDirectory)); return regularExpression.test(normalizedGitDirectory); }; const shouldIncludeConfigSection = (section, gitDirectory, configFilePath) => { if (section?.toLowerCase() === 'include') { return true; } // `globalGitignore` intentionally keeps `includeIf` support narrow. // Only `gitdir:` and `gitdir/i:` conditions are treated as active here. // Other Git predicates such as `onbranch:` are outside this feature's // supported boundary and are documented as unsupported. const condition = parseIncludeIfCondition(section); return condition ? matchesIncludeIfCondition(condition, gitDirectory, configFilePath) : false; }; const createExcludesFileValue = (value, declaringFilePath) => ({ value, declaringFilePath, }); /** Parse git config content and return the excludesFile value and any include paths to recurse into. The caller is responsible for reading files and recursing (sync or async). */ const parseGitConfigForExcludesFile = (content, normalizedPath, gitDirectory) => { let currentSection; let excludesFile; const includePaths = []; for (const line of content.split(/\r?\n/)) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith(';')) { continue; } if (trimmed.startsWith('[')) { currentSection = parseGitConfigSection(trimmed); continue; } const entry = parseGitConfigEntry(trimmed); if (!entry) { continue; } if (currentSection?.toLowerCase() === 'core' && entry.key === 'excludesfile') { excludesFile = createExcludesFileValue(entry.value, normalizedPath); continue; } if (shouldIncludeConfigSection(currentSection, gitDirectory, normalizedPath) && entry.key === 'path' && entry.value) { includePaths.push(resolveConfigPath(normalizedPath, entry.value)); } } return {excludesFile, includePaths}; }; const readGitConfigFile = (normalizedPath, readMethod, suppressErrors) => { try { return readMethod(normalizedPath, 'utf8'); } catch (error) { if (shouldSkipIgnoreFileError(error, suppressErrors)) { return undefined; } throw createGitConfigReadError(normalizedPath, error); } }; const getExcludesFileFromGitConfigSync = (filePath, readFileSync, gitDirectory, options = {}) => { const {suppressErrors, includeStack = new Set(), depth = 0} = options; const normalizedPath = path.resolve(filePath); if (includeStack.has(normalizedPath)) { return undefined; } if (depth >= MAX_INCLUDE_DEPTH) { return undefined; } includeStack.add(normalizedPath); const content = readGitConfigFile(normalizedPath, readFileSync, suppressErrors); if (content === undefined) { includeStack.delete(normalizedPath); return undefined; } let {excludesFile, includePaths} = parseGitConfigForExcludesFile(content, normalizedPath, gitDirectory); for (const includePath of includePaths) { const includedExcludesFile = getExcludesFileFromGitConfigSync(includePath, readFileSync, gitDirectory, {suppressErrors, includeStack, depth: depth + 1}); if (includedExcludesFile !== undefined) { excludesFile = includedExcludesFile; } } includeStack.delete(normalizedPath); return excludesFile; }; const getExcludesFileFromGitConfigAsync = async (filePath, readFile, gitDirectory, options = {}) => { const {suppressErrors, includeStack = new Set(), depth = 0} = options; const normalizedPath = path.resolve(filePath); if (includeStack.has(normalizedPath)) { return undefined; } if (depth >= MAX_INCLUDE_DEPTH) { return undefined; } includeStack.add(normalizedPath); let content; try { content = await readFile(normalizedPath, 'utf8'); } catch (error) { includeStack.delete(normalizedPath); if (shouldSkipIgnoreFileError(error, suppressErrors)) { return undefined; } throw createGitConfigReadError(normalizedPath, error); } let {excludesFile, includePaths} = parseGitConfigForExcludesFile(content, normalizedPath, gitDirectory); for (const includePath of includePaths) { // eslint-disable-next-line no-await-in-loop const includedExcludesFile = await getExcludesFileFromGitConfigAsync(includePath, readFile, gitDirectory, {suppressErrors, includeStack, depth: depth + 1}); if (includedExcludesFile !== undefined) { excludesFile = includedExcludesFile; } } includeStack.delete(normalizedPath); return excludesFile; }; const resolveGitDirectoryFromFile = (gitFilePath, content) => { const match = content.match(/^gitdir:\s*(.+?)\s*$/i); if (!match) { return gitFilePath; } return path.resolve(path.dirname(gitFilePath), match[1]); }; const getGitDirectorySync = (gitRoot, readFileSync) => { if (!gitRoot) { return undefined; } const gitFilePath = path.join(gitRoot, '.git'); try { return resolveGitDirectoryFromFile(gitFilePath, readFileSync(gitFilePath, 'utf8')); } catch { return gitFilePath; } }; const getGitDirectoryAsync = async (gitRoot, readFile) => { if (!gitRoot) { return undefined; } const gitFilePath = path.join(gitRoot, '.git'); try { return resolveGitDirectoryFromFile(gitFilePath, await readFile(gitFilePath, 'utf8')); } catch { return gitFilePath; } }; const getXdgConfigHome = () => process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'); const getGitConfigPaths = () => { // `globalGitignore` intentionally reads only user-level Git config. // It does not try to emulate every Git config scope such as repository // `.git/config` or system config. This keeps the feature boundary small // and predictable while still covering the common user-level excludes file. // // `GIT_CONFIG_GLOBAL` replaces the user-level config entirely. if ('GIT_CONFIG_GLOBAL' in process.env) { const value = process.env.GIT_CONFIG_GLOBAL; return value ? [value] : []; } return [ path.join(getXdgConfigHome(), 'git', 'config'), path.join(os.homedir(), '.gitconfig'), ]; }; const getDefaultGlobalGitignorePath = () => path.join(getXdgConfigHome(), 'git', 'ignore'); const resolveExcludesFilePath = excludesFileConfig => { // An explicit empty value disables the global gitignore entirely. if (excludesFileConfig?.value === '') { return undefined; } // When no core.excludesFile was configured, fall back to Git's default // user-level ignore file. This matches Git's behavior: the default path // applies even when GIT_CONFIG_GLOBAL="" suppresses config file reading. if (excludesFileConfig === undefined) { return getDefaultGlobalGitignorePath(); } // Relative core.excludesfile values are resolved from the config file that // declared them. Do not resolve them from the repository root. return resolveConfigPath(excludesFileConfig.declaringFilePath, excludesFileConfig.value); }; const readGlobalGitignoreContent = (filePath, readMethod, suppressErrors) => { try { const content = readMethod(filePath, 'utf8'); return {filePath, content}; } catch (error) { if (shouldSkipIgnoreFileError(error, suppressErrors)) { return undefined; } throw createIgnoreFileReadError(filePath, error); } }; export const getGlobalGitignoreFile = (options = {}) => { const cwd = toPath(options.cwd) ?? process.cwd(); const readFileSync = getReadFileSyncMethod(options.fs); const gitRoot = findGitRootSync(cwd, options.fs); const gitDirectory = getGitDirectorySync(gitRoot, readFileSync); let excludesFileConfig; for (const gitConfigPath of getGitConfigPaths()) { const value = getExcludesFileFromGitConfigSync(gitConfigPath, readFileSync, gitDirectory, {suppressErrors: options.suppressErrors}); if (value !== undefined) { excludesFileConfig = value; } } const filePath = resolveExcludesFilePath(excludesFileConfig); return filePath === undefined ? undefined : readGlobalGitignoreContent(filePath, readFileSync, options.suppressErrors); }; export const getGlobalGitignoreFileAsync = async (options = {}) => { const cwd = toPath(options.cwd) ?? process.cwd(); const readFile = getReadFileMethod(options.fs); const gitRoot = await findGitRoot(cwd, options.fs); const gitDirectory = await getGitDirectoryAsync(gitRoot, readFile); const excludesFileValues = await Promise.all(getGitConfigPaths().map(gitConfigPath => getExcludesFileFromGitConfigAsync( gitConfigPath, readFile, gitDirectory, {suppressErrors: options.suppressErrors}, ))); const excludesFileConfig = excludesFileValues.findLast(value => value !== undefined); const filePath = resolveExcludesFilePath(excludesFileConfig); if (filePath === undefined) { return undefined; } try { const content = await readFile(filePath, 'utf8'); return {filePath, content}; } catch (error) { if (shouldSkipIgnoreFileError(error, options.suppressErrors)) { return undefined; } throw createIgnoreFileReadError(filePath, error); } }; export const buildGlobalMatcher = (globalIgnoreFile, cwd, rootDirectory = cwd) => { // Passing the file's own directory as cwd gives base='', so patterns stay // unchanged and are interpreted relative to the project root (cwd). This // matches Git's behavior: patterns without slashes match at any depth, // patterns starting with / are anchored to the project root. const patterns = parseIgnoreFile(globalIgnoreFile, path.dirname(globalIgnoreFile.filePath)); return createIgnoreMatcher(patterns, cwd, rootDirectory); }; export const buildGlobalPredicate = (globalIgnoreFile, cwd, rootDirectory = cwd) => { const matcher = buildGlobalMatcher(globalIgnoreFile, cwd, rootDirectory); return fileOrDirectory => matcher(fileOrDirectory).ignored; }; const collectIgnoreFileArtifactsAsync = async (patterns, options, includeParentIgnoreFiles) => { const normalizedOptions = normalizeOptions(options); const childPaths = await globIgnoreFiles(fastGlob, patterns, normalizedOptions); const gitRoot = includeParentIgnoreFiles ? await findGitRoot(normalizedOptions.cwd, normalizedOptions.fs) : undefined; const allPaths = combineIgnoreFilePaths(gitRoot, normalizedOptions, childPaths); const readFileMethod = getReadFileMethod(normalizedOptions.fs); const files = await readIgnoreFilesSafely(allPaths, readFileMethod, normalizedOptions.suppressErrors); return {files, normalizedOptions, gitRoot}; }; const collectIgnoreFileArtifactsSync = (patterns, options, includeParentIgnoreFiles) => { const normalizedOptions = normalizeOptions(options); const childPaths = globIgnoreFiles(fastGlob.sync, patterns, normalizedOptions); const gitRoot = includeParentIgnoreFiles ? findGitRootSync(normalizedOptions.cwd, normalizedOptions.fs) : undefined; const allPaths = combineIgnoreFilePaths(gitRoot, normalizedOptions, childPaths); const readFileSyncMethod = getReadFileSyncMethod(normalizedOptions.fs); const files = readIgnoreFilesSafelySync(allPaths, readFileSyncMethod, normalizedOptions.suppressErrors); return {files, normalizedOptions, gitRoot}; }; export const isIgnoredByIgnoreFiles = async (patterns, options) => { const {files, normalizedOptions, gitRoot} = await collectIgnoreFileArtifactsAsync(patterns, options, false); return buildIgnoreResult(files, normalizedOptions, gitRoot).predicate; }; export const isIgnoredByIgnoreFilesSync = (patterns, options) => { const {files, normalizedOptions, gitRoot} = collectIgnoreFileArtifactsSync(patterns, options, false); return buildIgnoreResult(files, normalizedOptions, gitRoot).predicate; }; const getPatternsFromIgnoreFiles = (files, baseDir) => files.flatMap(file => parseIgnoreFile(file, baseDir)); /** Read ignore files and return both patterns and predicate. This avoids reading the same files twice (once for patterns, once for filtering). @param {string[]} patterns - Patterns to find ignore files @param {Object} options - Options object @param {boolean} [includeParentIgnoreFiles=false] - Whether to search for parent .gitignore files @returns {Promise<{patterns: string[], matcher: Function, predicate: Function, usingGitRoot: boolean}>} */ export const getIgnorePatternsAndPredicate = async (patterns, options, includeParentIgnoreFiles = false) => { const {files, normalizedOptions, gitRoot} = await collectIgnoreFileArtifactsAsync( patterns, options, includeParentIgnoreFiles, ); return buildIgnoreResult(files, normalizedOptions, gitRoot); }; /** Read ignore files and return both patterns and predicate (sync version). @param {string[]} patterns - Patterns to find ignore files @param {Object} options - Options object @param {boolean} [includeParentIgnoreFiles=false] - Whether to search for parent .gitignore files @returns {{patterns: string[], matcher: Function, predicate: Function, usingGitRoot: boolean}} */ export const getIgnorePatternsAndPredicateSync = (patterns, options, includeParentIgnoreFiles = false) => { const {files, normalizedOptions, gitRoot} = collectIgnoreFileArtifactsSync( patterns, options, includeParentIgnoreFiles, ); return buildIgnoreResult(files, normalizedOptions, gitRoot); }; export const isGitIgnored = options => isIgnoredByIgnoreFiles(GITIGNORE_FILES_PATTERN, options); export const isGitIgnoredSync = options => isIgnoredByIgnoreFilesSync(GITIGNORE_FILES_PATTERN, options);