Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ignoreFiles option #228

Merged
merged 18 commits into from Jan 25, 2022
1 change: 1 addition & 0 deletions fixtures/ignore-files/.eslintignore
@@ -0,0 +1 @@
ignored-by-eslint.js
1 change: 1 addition & 0 deletions fixtures/ignore-files/.prettierignore
@@ -0,0 +1 @@
ignored-by-prettier.js
20 changes: 13 additions & 7 deletions gitignore.js → ignore.js
Expand Up @@ -6,21 +6,24 @@ import gitIgnore from 'ignore';
import slash from 'slash';
import {toPath, isNegativePattern} from './utilities.js';

const gitignoreGlobOptions = {
const ignoreFilesGlobOptions = {
ignore: [
'**/node_modules',
'**/flow-typed',
'**/coverage',
'**/.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
Expand All @@ -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 => {
Expand All @@ -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 => ({
Expand All @@ -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,
Expand All @@ -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);
9 changes: 9 additions & 0 deletions index.d.ts
Expand Up @@ -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.

Expand Down
37 changes: 29 additions & 8 deletions index.js
Expand Up @@ -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 => {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -201,4 +222,4 @@ export const generateGlobTasksSync = normalizeArgumentsSync(generateTasksSync);
export {
isGitIgnored,
isGitIgnoredSync,
} from './gitignore.js';
} from './ignore.js';
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -22,7 +22,7 @@
"files": [
"index.js",
"index.d.ts",
"gitignore.js",
"ignore.js",
"utilities.js"
],
"keywords": [
Expand Down
11 changes: 10 additions & 1 deletion readme.md
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
15 changes: 15 additions & 0 deletions tests/globby.js
Expand Up @@ -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(
Expand Down
83 changes: 76 additions & 7 deletions tests/gitignore.js → 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});
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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));
Expand All @@ -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});
Expand All @@ -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));
Expand Down Expand Up @@ -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',
],
);
});