diff --git a/fixtures/ignore-files/.eslintignore b/fixtures/ignore-files/.eslintignore new file mode 100644 index 0000000..83217a1 --- /dev/null +++ b/fixtures/ignore-files/.eslintignore @@ -0,0 +1 @@ +ignored-by-eslint.js diff --git a/fixtures/ignore-files/.prettierignore b/fixtures/ignore-files/.prettierignore new file mode 100644 index 0000000..074b6c6 --- /dev/null +++ b/fixtures/ignore-files/.prettierignore @@ -0,0 +1 @@ +ignored-by-prettier.js diff --git a/gitignore.js b/ignore.js similarity index 72% rename from gitignore.js rename to ignore.js index 13ce123..c6172f9 100644 --- a/gitignore.js +++ b/ignore.js @@ -6,7 +6,7 @@ import gitIgnore from 'ignore'; import slash from 'slash'; import {toPath, isNegativePattern} from './utilities.js'; -const gitignoreGlobOptions = { +const ignoreFilesGlobOptions = { ignore: [ '**/node_modules', '**/flow-typed', @@ -14,13 +14,16 @@ const gitignoreGlobOptions = { '**/.git', ], absolute: true, + dot: true, }; +export const GITIGNORE_FILES_PATTERN = '**/.gitignore'; + const applyBaseToPattern = (pattern, base) => isNegativePattern(pattern) ? '!' + path.posix.join(base, pattern.slice(1)) : path.posix.join(base, pattern); -const parseGitIgnoreFile = (file, cwd) => { +const parseIgnoreFile = (file, cwd) => { const base = slash(path.relative(cwd, path.dirname(file.filePath))); return file.content @@ -43,7 +46,7 @@ const toRelativePath = (fileOrDirectory, cwd) => { }; const getIsIgnoredPredicate = (files, cwd) => { - const patterns = files.flatMap(file => parseGitIgnoreFile(file, cwd)); + const patterns = files.flatMap(file => parseIgnoreFile(file, cwd)); const ignores = gitIgnore().add(patterns); return fileOrDirectory => { @@ -57,10 +60,10 @@ const normalizeOptions = (options = {}) => ({ cwd: toPath(options.cwd) || process.cwd(), }); -export const isGitIgnored = async options => { +export const isIgnoredByIgnoreFiles = async (patterns, options) => { const {cwd} = normalizeOptions(options); - const paths = await fastGlob('**/.gitignore', {cwd, ...gitignoreGlobOptions}); + const paths = await fastGlob(patterns, {cwd, ...ignoreFilesGlobOptions}); const files = await Promise.all( paths.map(async filePath => ({ @@ -72,10 +75,10 @@ export const isGitIgnored = async options => { return getIsIgnoredPredicate(files, cwd); }; -export const isGitIgnoredSync = options => { +export const isIgnoredByIgnoreFilesSync = (patterns, options) => { const {cwd} = normalizeOptions(options); - const paths = fastGlob.sync('**/.gitignore', {cwd, ...gitignoreGlobOptions}); + const paths = fastGlob.sync(patterns, {cwd, ...ignoreFilesGlobOptions}); const files = paths.map(filePath => ({ filePath, @@ -84,3 +87,6 @@ export const isGitIgnoredSync = options => { return getIsIgnoredPredicate(files, cwd); }; + +export const isGitIgnored = options => isIgnoredByIgnoreFiles(GITIGNORE_FILES_PATTERN, options); +export const isGitIgnoredSync = options => isIgnoredByIgnoreFilesSync(GITIGNORE_FILES_PATTERN, options); diff --git a/index.d.ts b/index.d.ts index ae46f2a..b8d817a 100644 --- a/index.d.ts +++ b/index.d.ts @@ -47,6 +47,15 @@ export interface Options extends FastGlobOptionsWithoutCwd { */ readonly gitignore?: boolean; + /** + Glob patterns to look for ignore files, which are then used to ignore globbed files. + + This is a more generic form of the `gitignore` option, allowing you to find ignore files with a [compatible syntax](http://git-scm.com/docs/gitignore). For instance, this works with Babel's `.babelignore`, Prettier's `.prettierignore`, or ESLint's `.eslintignore` files. + + @default undefined + */ + readonly ignoreFiles?: string | string[]; + /** The current working directory in which to search. diff --git a/index.js b/index.js index d68283b..f7a8000 100644 --- a/index.js +++ b/index.js @@ -2,7 +2,11 @@ import fs from 'node:fs'; import merge2 from 'merge2'; import fastGlob from 'fast-glob'; import dirGlob from 'dir-glob'; -import {isGitIgnored, isGitIgnoredSync} from './gitignore.js'; +import { + GITIGNORE_FILES_PATTERN, + isIgnoredByIgnoreFiles, + isIgnoredByIgnoreFilesSync, +} from './ignore.js'; import {FilterStream, toPath, isNegativePattern} from './utilities.js'; const assertPatternsInput = patterns => { @@ -50,13 +54,30 @@ const normalizeOptions = (options = {}) => { const normalizeArguments = fn => async (patterns, options) => fn(toPatternsArray(patterns), normalizeOptions(options)); const normalizeArgumentsSync = fn => (patterns, options) => fn(toPatternsArray(patterns), normalizeOptions(options)); -const getFilter = async options => createFilterFunction( - options.gitignore && await isGitIgnored({cwd: options.cwd}), -); +const getIgnoreFilesPatterns = options => { + const {ignoreFiles, gitignore} = options; -const getFilterSync = options => createFilterFunction( - options.gitignore && isGitIgnoredSync({cwd: options.cwd}), -); + const patterns = ignoreFiles ? toPatternsArray(ignoreFiles) : []; + if (gitignore) { + patterns.push(GITIGNORE_FILES_PATTERN); + } + + return patterns; +}; + +const getFilter = async options => { + const ignoreFilesPatterns = getIgnoreFilesPatterns(options); + return createFilterFunction( + ignoreFilesPatterns.length > 0 && await isIgnoredByIgnoreFiles(ignoreFilesPatterns, {cwd: options.cwd}), + ); +}; + +const getFilterSync = options => { + const ignoreFilesPatterns = getIgnoreFilesPatterns(options); + return createFilterFunction( + ignoreFilesPatterns.length > 0 && isIgnoredByIgnoreFilesSync(ignoreFilesPatterns, {cwd: options.cwd}), + ); +}; const createFilterFunction = isIgnored => { const seen = new Set(); @@ -201,4 +222,4 @@ export const generateGlobTasksSync = normalizeArgumentsSync(generateTasksSync); export { isGitIgnored, isGitIgnoredSync, -} from './gitignore.js'; +} from './ignore.js'; diff --git a/package.json b/package.json index 016f4b0..2322f2d 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "files": [ "index.js", "index.d.ts", - "gitignore.js", + "ignore.js", "utilities.js" ], "keywords": [ diff --git a/readme.md b/readme.md index 8ddca37..ff36479 100644 --- a/readme.md +++ b/readme.md @@ -10,7 +10,7 @@ Based on [`fast-glob`](https://github.com/mrmlnc/fast-glob) but adds a bunch of - Multiple patterns - Negated patterns: `['foo*', '!foobar']` - Expands directories: `foo` → `foo/**/*` -- Supports `.gitignore` +- Supports `.gitignore` and similar ignore config files - Supports `URL` as `cwd` ## Install @@ -88,6 +88,15 @@ Default: `false` Respect ignore patterns in `.gitignore` files that apply to the globbed files. +##### ignoreFiles + +Type: `string | string[]`\ +Default: `undefined` + +Glob patterns to look for ignore files, which are then used to ignore globbed files. + +This is a more generic form of the `gitignore` option, allowing you to find ignore files with a [compatible syntax](http://git-scm.com/docs/gitignore). For instance, this works with Babel's `.babelignore`, Prettier's `.prettierignore`, or ESLint's `.eslintignore` files. + ### globbySync(patterns, options?) Returns `string[]` of matching paths. diff --git a/tests/globby.js b/tests/globby.js index 4abd235..c141c7b 100644 --- a/tests/globby.js +++ b/tests/globby.js @@ -241,6 +241,21 @@ test('gitignore option and objectMode option', async t => { t.truthy(result[0].path); }); +test('respects ignoreFiles string option', async t => { + const actual = await runGlobby(t, '*', {gitignore: false, ignoreFiles: '.gitignore', onlyFiles: false}); + t.false(actual.includes('node_modules')); +}); + +test('respects ignoreFiles array option', async t => { + const actual = await runGlobby(t, '*', {gitignore: false, ignoreFiles: ['.gitignore'], onlyFiles: false}); + t.false(actual.includes('node_modules')); +}); + +test('glob dot files', async t => { + const actual = await runGlobby(t, '*', {gitignore: false, ignoreFiles: '*gitignore', onlyFiles: false}); + t.false(actual.includes('node_modules')); +}); + test('`{extension: false}` and `expandDirectories.extensions` option', async t => { for (const temporaryDirectory of getPathValues(temporary)) { t.deepEqual( diff --git a/tests/gitignore.js b/tests/ignore.js similarity index 62% rename from tests/gitignore.js rename to tests/ignore.js index 7dd04b6..4153fe8 100644 --- a/tests/gitignore.js +++ b/tests/ignore.js @@ -1,13 +1,18 @@ import path from 'node:path'; import test from 'ava'; import slash from 'slash'; -import {isGitIgnored, isGitIgnoredSync} from '../gitignore.js'; +import { + isIgnoredByIgnoreFiles, + isIgnoredByIgnoreFilesSync, + isGitIgnored, + isGitIgnoredSync, +} from '../ignore.js'; import { PROJECT_ROOT, getPathValues, } from './utilities.js'; -test('gitignore', async t => { +test('ignore', async t => { for (const cwd of getPathValues(path.join(PROJECT_ROOT, 'fixtures/gitignore'))) { // eslint-disable-next-line no-await-in-loop const isIgnored = await isGitIgnored({cwd}); @@ -17,7 +22,7 @@ test('gitignore', async t => { } }); -test('gitignore - mixed path styles', async t => { +test('ignore - mixed path styles', async t => { const directory = path.join(PROJECT_ROOT, 'fixtures/gitignore'); for (const cwd of getPathValues(directory)) { // eslint-disable-next-line no-await-in-loop @@ -26,7 +31,7 @@ test('gitignore - mixed path styles', async t => { } }); -test('gitignore - os paths', async t => { +test('ignore - os paths', async t => { const directory = path.join(PROJECT_ROOT, 'fixtures/gitignore'); for (const cwd of getPathValues(directory)) { // eslint-disable-next-line no-await-in-loop @@ -35,7 +40,7 @@ test('gitignore - os paths', async t => { } }); -test('gitignore - sync', t => { +test('ignore - sync', t => { for (const cwd of getPathValues(path.join(PROJECT_ROOT, 'fixtures/gitignore'))) { const isIgnored = isGitIgnoredSync({cwd}); const actual = ['foo.js', 'bar.js'].filter(file => !isIgnored(file)); @@ -44,7 +49,7 @@ test('gitignore - sync', t => { } }); -test('negative gitignore', async t => { +test('negative ignore', async t => { for (const cwd of getPathValues(path.join(PROJECT_ROOT, 'fixtures/negative'))) { // eslint-disable-next-line no-await-in-loop const isIgnored = await isGitIgnored({cwd}); @@ -54,7 +59,7 @@ test('negative gitignore', async t => { } }); -test('negative gitignore - sync', t => { +test('negative ignore - sync', t => { for (const cwd of getPathValues(path.join(PROJECT_ROOT, 'fixtures/negative'))) { const isIgnored = isGitIgnoredSync({cwd}); const actual = ['foo.js', 'bar.js'].filter(file => !isIgnored(file)); @@ -111,3 +116,67 @@ test('check file', async t => { t.false(isIgnored(file)); } }); + +test('custom ignore files - sync', t => { + const cwd = path.join(PROJECT_ROOT, 'fixtures/ignore-files'); + const files = [ + 'ignored-by-eslint.js', + 'ignored-by-prettier.js', + 'not-ignored.js', + ]; + + const isEslintIgnored = isIgnoredByIgnoreFilesSync('.eslintignore', {cwd}); + const isPrettierIgnored = isIgnoredByIgnoreFilesSync('.prettierignore', {cwd}); + const isEslintOrPrettierIgnored = isIgnoredByIgnoreFilesSync('.{prettier,eslint}ignore', {cwd}); + t.deepEqual( + files.filter(file => isEslintIgnored(file)), + [ + 'ignored-by-eslint.js', + ], + ); + t.deepEqual( + files.filter(file => isPrettierIgnored(file)), + [ + 'ignored-by-prettier.js', + ], + ); + t.deepEqual( + files.filter(file => isEslintOrPrettierIgnored(file)), + [ + 'ignored-by-eslint.js', + 'ignored-by-prettier.js', + ], + ); +}); + +test('custom ignore files - async', async t => { + const cwd = path.join(PROJECT_ROOT, 'fixtures/ignore-files'); + const files = [ + 'ignored-by-eslint.js', + 'ignored-by-prettier.js', + 'not-ignored.js', + ]; + + const isEslintIgnored = await isIgnoredByIgnoreFiles('.eslintignore', {cwd}); + const isPrettierIgnored = await isIgnoredByIgnoreFiles('.prettierignore', {cwd}); + const isEslintOrPrettierIgnored = await isIgnoredByIgnoreFiles('.{prettier,eslint}ignore', {cwd}); + t.deepEqual( + files.filter(file => isEslintIgnored(file)), + [ + 'ignored-by-eslint.js', + ], + ); + t.deepEqual( + files.filter(file => isPrettierIgnored(file)), + [ + 'ignored-by-prettier.js', + ], + ); + t.deepEqual( + files.filter(file => isEslintOrPrettierIgnored(file)), + [ + 'ignored-by-eslint.js', + 'ignored-by-prettier.js', + ], + ); +});