Skip to content

Commit 9b51352

Browse files
kirkwaiblingermdjermanovicfasttimelumirlumir
authored
feat: Add new includeIgnoreFile() to config-helpers (#430)
Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com> Co-authored-by: Francesco Trotta <github@fasttime.org> Co-authored-by: 루밀LuMir <rpfos@naver.com>
1 parent 70b6997 commit 9b51352

12 files changed

Lines changed: 604 additions & 6 deletions

File tree

packages/compat/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ This package exports the following functions in both ESM and CommonJS format:
3333
- `fixupRule(rule)` - wraps the given rule in a compatibility layer and returns the result
3434
- `fixupPluginRules(plugin)` - wraps each rule in the given plugin using `fixupRule()` and returns a new object that represents the plugin with the fixed-up rules
3535
- `fixupConfigRules(configs)` - wraps all plugins found in an array of config objects using `fixupPluginRules()`
36-
- `includeIgnoreFile(path)` - reads an ignore file (like `.gitignore`) and converts the patterns into the correct format for the config file
36+
- `includeIgnoreFile(path)` (deprecated) - reads an ignore file (like `.gitignore`) and converts the patterns into the correct format for the config file
3737

3838
### Fixing Rules
3939

@@ -149,6 +149,8 @@ module.exports = defineConfig([
149149

150150
### Including Ignore Files
151151

152+
**Deprecated**: The `includeIgnoreFile()` exported by this package has been deprecated ([eslint/rewrite#329](https://github.com/eslint/rewrite/issues/329)). Use the `includeIgnoreFile()` function exported by `@eslint/config-helpers` instead (also available at `eslint/config`). This section is only preserved for historical reference.
153+
152154
If you were using an alternate ignore file in ESLint v8.x, such as using `--ignore-path .gitignore` on the command line, you can include those patterns programmatically in your config file using the `includeIgnoreFile()` function.
153155

154156
The `includeIgnoreFile()` function also accepts a second optional `name` parameter that allows you to set a custom name for this configuration object. If not specified, it defaults to `"Imported .gitignore patterns"`. For example:

packages/compat/src/ignore-file.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ import path from "node:path";
2424
* Converts an ESLint ignore pattern to a minimatch pattern.
2525
* @param {string} pattern The .eslintignore or .gitignore pattern to convert.
2626
* @returns {string} The converted pattern.
27+
*
28+
* @deprecated Use the `convertIgnorePatternToMinimatch()` function exported by
29+
* `@eslint/config-helpers` instead.
2730
*/
2831
export function convertIgnorePatternToMinimatch(pattern) {
2932
const isNegated = pattern.startsWith("!");
@@ -72,6 +75,9 @@ export function convertIgnorePatternToMinimatch(pattern) {
7275
* @param {string} [name] The name of the ignore file config.
7376
* @returns {FlatConfig} An object with an `ignores` property that is an array of ignore patterns.
7477
* @throws {Error} If the ignore file path is not an absolute path.
78+
*
79+
* @deprecated Use the `includeIgnoreFile()` function exported by
80+
* `@eslint/config-helpers` instead (also available at `eslint/config`).
7581
*/
7682
export function includeIgnoreFile(ignoreFilePath, name) {
7783
if (!path.isAbsolute(ignoreFilePath)) {

packages/compat/tests/ignore-file.test.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/**
2-
* @filedescription Fixup tests
2+
* @fileoverview Tests for `includeIgnoreFile()` and `convertIgnorePatternToMinimatch()`
3+
* @author Nicholas C. Zakas
34
*/
45

56
//-----------------------------------------------------------------------------

packages/config-helpers/README.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,59 @@ export default defineConfig([
7474
]);
7575
```
7676

77+
### `includeIgnoreFile()`
78+
79+
The `includeIgnoreFile()` function reads a file with gitignore-style patterns (such as a `.gitignore`) and returns a config object with the patterns converted to a global ignores object. Pass the absolute path to the ignore file as the first argument:
80+
81+
```js
82+
// eslint.config.js
83+
84+
import { defineConfig, includeIgnoreFile } from "@eslint/config-helpers";
85+
import path from "node:path";
86+
87+
const ignorePath = path.join(import.meta.dirname, ".gitignore");
88+
89+
export default defineConfig([
90+
includeIgnoreFile(ignorePath, {
91+
gitignoreResolution: true,
92+
}),
93+
// ...
94+
]);
95+
```
96+
97+
#### Options
98+
99+
The second argument is an optional options object:
100+
101+
- **`gitignoreResolution`** (`boolean`): Controls how ignore patterns are interpreted.
102+
- `false` (default) — patterns are resolved relative to the location of the configuration file.
103+
- `true` — patterns are resolved relative to the location of the ignore file, matching the behavior of `.gitignore` files.
104+
- **`name`** (`string`): A custom name for the resulting config object.
105+
106+
For backwards compatibility with `includeIgnoreFile()` from `@eslint/compat`, passing a string instead of an object as the second argument is treated as equivalent to providing a value for `name`.
107+
108+
#### Multiple files
109+
110+
You can also pass an array of absolute paths to include multiple ignore files at once. In this case an array of config objects is returned:
111+
112+
```js
113+
// eslint.config.js
114+
115+
import { defineConfig, includeIgnoreFile } from "@eslint/config-helpers";
116+
import path from "node:path";
117+
118+
export default defineConfig([
119+
includeIgnoreFile(
120+
[
121+
path.join(import.meta.dirname, ".gitignore"),
122+
path.join(import.meta.dirname, "packages/lib/.gitignore"),
123+
],
124+
{ gitignoreResolution: true },
125+
),
126+
// ...
127+
]);
128+
```
129+
77130
## License
78131
79132
Apache 2.0
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
/**
2+
* @fileoverview Ignore file utilities for the config-helpers package.
3+
* This file was forked from the source code for the compat package.
4+
*
5+
* @author Nicholas C. Zakas
6+
* @author Kirk Waiblinger
7+
*/
8+
9+
//-----------------------------------------------------------------------------
10+
// Imports
11+
//-----------------------------------------------------------------------------
12+
13+
import fs from "node:fs";
14+
import path from "node:path";
15+
16+
//-----------------------------------------------------------------------------
17+
// Types
18+
//-----------------------------------------------------------------------------
19+
20+
/** @typedef {import("@eslint/core").ConfigObject} Config */
21+
22+
/**
23+
* @typedef {object} IncludeIgnoreFileOptionsObject
24+
* @property {boolean} [gitignoreResolution] Whether to interpret the contents of an ignore file relative to the config file or the ignore file.
25+
* - gitignoreResolution: false (default): Interprets ignore patterns relative to the config file
26+
* - gitignoreResolution: true: Interprets the ignore patterns in a file relative to the ignore file
27+
* @property {string} [name] The name to give the output config object(s).
28+
*/
29+
30+
/**
31+
* Options for `includeIgnoreFile()`. May be provided as an object or, for
32+
* legacy compatibility with `@eslint/compat`, as a string which is treated as
33+
* the `name` option.
34+
* @typedef {IncludeIgnoreFileOptionsObject | string} IncludeIgnoreFileOptions
35+
*/
36+
37+
//-----------------------------------------------------------------------------
38+
// Exports
39+
//-----------------------------------------------------------------------------
40+
41+
/**
42+
* Converts an ESLint ignore pattern to a minimatch pattern.
43+
* @param {string} pattern The .eslintignore or .gitignore pattern to convert.
44+
* @returns {string} The converted pattern.
45+
*/
46+
export function convertIgnorePatternToMinimatch(pattern) {
47+
const isNegated = pattern.startsWith("!");
48+
const negatedPrefix = isNegated ? "!" : "";
49+
const patternToTest = (isNegated ? pattern.slice(1) : pattern).trimEnd();
50+
51+
// special cases
52+
if (["", "**", "/**", "**/"].includes(patternToTest)) {
53+
return `${negatedPrefix}${patternToTest}`;
54+
}
55+
56+
const firstIndexOfSlash = patternToTest.indexOf("/");
57+
58+
const matchEverywherePrefix =
59+
firstIndexOfSlash < 0 || firstIndexOfSlash === patternToTest.length - 1
60+
? "**/"
61+
: "";
62+
63+
const patternWithoutLeadingSlash =
64+
firstIndexOfSlash === 0 ? patternToTest.slice(1) : patternToTest;
65+
66+
/*
67+
* Escape `{` and `(` because in gitignore patterns they are just
68+
* literal characters without any specific syntactic meaning,
69+
* while in minimatch patterns they can form brace expansion or extglob syntax.
70+
*
71+
* For example, gitignore pattern `src/{a,b}.js` ignores file `src/{a,b}.js`.
72+
* But, the same minimatch pattern `src/{a,b}.js` ignores files `src/a.js` and `src/b.js`.
73+
* Minimatch pattern `src/\{a,b}.js` is equivalent to gitignore pattern `src/{a,b}.js`.
74+
*/
75+
const escapedPatternWithoutLeadingSlash =
76+
patternWithoutLeadingSlash.replaceAll(
77+
// eslint-disable-next-line regexp/no-empty-lookarounds-assertion -- False positive
78+
/(?=((?:\\.|[^{(])*))\1([{(])/guy,
79+
"$1\\$2",
80+
);
81+
82+
const matchInsideSuffix = patternToTest.endsWith("/**") ? "/*" : "";
83+
84+
return `${negatedPrefix}${matchEverywherePrefix}${escapedPatternWithoutLeadingSlash}${matchInsideSuffix}`;
85+
}
86+
87+
/**
88+
* @param {string} ignoreFilePath
89+
* @returns {string[]}
90+
*/
91+
function ignoreFilePathToPatterns(ignoreFilePath) {
92+
const ignoreFile = fs.readFileSync(ignoreFilePath, "utf8");
93+
const lines = ignoreFile.split(/\r?\n/u);
94+
95+
return lines
96+
.map(line => line.trim())
97+
.filter(line => line && !line.startsWith("#"))
98+
.map(convertIgnorePatternToMinimatch);
99+
}
100+
101+
/**
102+
* Helper to parse and validate the options to `includeIgnoreFile()`
103+
*
104+
* @param {string | { gitignoreResolution?: unknown, name?: unknown } | undefined} options
105+
* @returns {{ gitignoreResolution: boolean, name: string }}
106+
*/
107+
function parseOptions(options) {
108+
// legacy compatibility with @eslint/compat's `includeIgnoreFile`
109+
if (typeof options === "string") {
110+
return { gitignoreResolution: false, name: options };
111+
}
112+
113+
const optionsObject = options ?? {};
114+
if (typeof optionsObject !== "object" || Array.isArray(optionsObject)) {
115+
throw new TypeError(
116+
"The options argument to `includeIgnoreFile()` should be an object or a string.",
117+
);
118+
}
119+
120+
const gitignoreResolution = optionsObject.gitignoreResolution ?? false;
121+
if (typeof gitignoreResolution !== "boolean") {
122+
throw new TypeError(
123+
"The `gitignoreResolution` option must be specified a boolean or omitted",
124+
);
125+
}
126+
127+
const name = optionsObject.name ?? `Imported .gitignore patterns`;
128+
if (typeof name !== "string") {
129+
throw new TypeError(
130+
"The `name` option must be specified as a string or omitted.",
131+
);
132+
}
133+
134+
return { gitignoreResolution, name };
135+
}
136+
137+
/**
138+
* @overload
139+
*
140+
* Reads ignore files and returns objects with the ignore patterns.
141+
*
142+
* @param {string[]} ignoreFilePathArg The paths of ignore files to include.
143+
* @param {IncludeIgnoreFileOptions} [options]
144+
* @returns {Config[]}
145+
*/
146+
147+
/**
148+
* @overload
149+
*
150+
* Reads an ignore file and returns an object with the ignore patterns.
151+
*
152+
* @param {string} ignoreFilePathArg The path of the ignore file to include.
153+
* @param {IncludeIgnoreFileOptions} [options]
154+
* @returns {Config}
155+
*/
156+
157+
/**
158+
* @overload
159+
*
160+
* Reads an ignore file(s) and returns an object(s) with the ignore patterns.
161+
*
162+
* @param {string[] | string} ignoreFilePathArg The path(s) of the ignore file(s) to include.
163+
* @param {IncludeIgnoreFileOptions} [options]
164+
* @returns {Config[] | Config}
165+
*/
166+
167+
/**
168+
* Reads an ignore file(s) and returns an object(s) with the ignore patterns.
169+
*
170+
* @param {string[] | string} ignoreFilePathArg The path(s) of the ignore file(s) to include.
171+
* @param {IncludeIgnoreFileOptions} [options]
172+
* @returns {Config[] | Config}
173+
*/
174+
export function includeIgnoreFile(ignoreFilePathArg, options) {
175+
const returnSingleObject = !Array.isArray(ignoreFilePathArg);
176+
const ignoreFilePaths = Array.isArray(ignoreFilePathArg)
177+
? ignoreFilePathArg
178+
: [ignoreFilePathArg];
179+
for (const ignorePath of ignoreFilePaths) {
180+
if (typeof ignorePath !== "string") {
181+
throw new TypeError(
182+
"The first argument to `includeIgnoreFile()` should be a string or array of strings",
183+
);
184+
}
185+
if (!path.isAbsolute(ignorePath)) {
186+
throw new Error(
187+
`The ignore file location must be an absolute path. Received ${ignorePath}`,
188+
);
189+
}
190+
}
191+
192+
const { gitignoreResolution, name } = parseOptions(options);
193+
194+
if (returnSingleObject) {
195+
return {
196+
name,
197+
ignores: ignoreFilePathToPatterns(ignoreFilePathArg),
198+
...(gitignoreResolution
199+
? { basePath: path.dirname(ignoreFilePathArg) }
200+
: {}),
201+
};
202+
}
203+
204+
return ignoreFilePaths.map((ignoreFilePath, i) => ({
205+
name: `${name} (${i})`,
206+
ignores: ignoreFilePathToPatterns(ignoreFilePath),
207+
...(gitignoreResolution
208+
? { basePath: path.dirname(ignoreFilePath) }
209+
: {}),
210+
}));
211+
}

packages/config-helpers/src/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,7 @@
44

55
export { defineConfig } from "./define-config.js";
66
export { globalIgnores } from "./global-ignores.js";
7+
export {
8+
includeIgnoreFile,
9+
convertIgnorePatternToMinimatch,
10+
} from "./ignore-file.js";
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Node.js
2+
node_modules
3+
!/fixtures/node_modules
4+
/dist
5+
6+
# Logs
7+
*.log
8+
9+
# Gatsby files
10+
.cache/
11+
12+
# vuepress build output
13+
.vuepress/dist
14+
15+
# other
16+
*/foo.js
17+
dir/**

0 commit comments

Comments
 (0)