Skip to content

Commit b5f74ed

Browse files
nzakasmdjermanovic
andauthored
feat: Add includeIgnoreFile() method (#47)
Co-authored-by: Milos Djermanovic <[email protected]>
1 parent 10d8200 commit b5f74ed

File tree

9 files changed

+240
-59
lines changed

9 files changed

+240
-59
lines changed

.github/workflows/release-please.yml

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -50,62 +50,62 @@ jobs:
5050
#-----------------------------------------------------------------------------
5151

5252
#-----------------------------------------------------------------------------
53-
# @eslint/migrate-config
53+
# @eslint/compat
5454
#-----------------------------------------------------------------------------
5555

56-
- name: Publish @eslint/migrate-config package to npm
57-
run: npm publish -w packages/migrate-config
58-
if: ${{ steps.release.outputs['packages/migrate-config--release_created'] }}
56+
- name: Publish @eslint/compat package to npm
57+
run: npm publish -w packages/compat
58+
if: ${{ steps.release.outputs['packages/compat--release_created'] }}
5959
env:
6060
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
6161

62-
# NOTE: No JSR package because JSR doesn't support CLIs
62+
- name: Publish @eslint/compat package to JSR
63+
run: |
64+
npm run build --if-present
65+
npx jsr publish
66+
working-directory: packages/compat
67+
if: ${{ steps.release.outputs['packages/compat--release_created'] }}
6368

6469
- name: Tweet Release Announcement
65-
run: npx @humanwhocodes/tweet "eslint/migrate-config v${{ steps.release.outputs['packages/migrate-config--major'] }}.${{ steps.release.outputs['packages/migrate-config--minor'] }}.${{ steps.release.outputs['packages/migrate-config--patch'] }} has been released!\n\n${{ github.event.repository.html_url }}/releases/tag/${{ steps.release.outputs['packages/migrate-config--tag_name'] }}"
66-
if: ${{ steps.release.outputs['packages/migrate-config--release_created'] }}
70+
run: npx @humanwhocodes/tweet "eslint/compat v${{ steps.release.outputs['packages/compat--major'] }}.${{ steps.release.outputs['packages/compat--minor'] }}.${{ steps.release.outputs['packages/compat--patch'] }} has been released!\n\n${{ github.event.repository.html_url }}/releases/tag/${{ steps.release.outputs['packages/compat--tag_name'] }}"
71+
if: ${{ steps.release.outputs['packages/compat--release_created'] }}
6772
env:
6873
TWITTER_CONSUMER_KEY: ${{ secrets.TWITTER_CONSUMER_KEY }}
6974
TWITTER_CONSUMER_SECRET: ${{ secrets.TWITTER_CONSUMER_SECRET }}
7075
TWITTER_ACCESS_TOKEN_KEY: ${{ secrets.TWITTER_ACCESS_TOKEN_KEY }}
7176
TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }}
7277

7378
- name: Toot Release Announcement
74-
run: npx @humanwhocodes/toot "eslint/migrate-config v${{ steps.release.outputs['packages/migrate-config--major'] }}.${{ steps.release.outputs['packages/migrate-config--minor'] }}.${{ steps.release.outputs['packages/migrate-config--patch'] }} has been released!\n\n${{ github.event.repository.html_url }}/releases/tag/${{ steps.release.outputs['packages/migrate-config--tag_name'] }}"
75-
if: ${{ steps.release.outputs['packages/migrate-config--release_created'] }}
79+
run: npx @humanwhocodes/toot "eslint/compat v${{ steps.release.outputs['packages/compat--major'] }}.${{ steps.release.outputs['packages/compat--minor'] }}.${{ steps.release.outputs['packages/compat--patch'] }} has been released!\n\n${{ github.event.repository.html_url }}/releases/tag/${{ steps.release.outputs['packages/compat--tag_name'] }}"
80+
if: ${{ steps.release.outputs['packages/compat--release_created'] }}
7681
env:
7782
MASTODON_ACCESS_TOKEN: ${{ secrets.MASTODON_ACCESS_TOKEN }}
7883
MASTODON_HOST: ${{ secrets.MASTODON_HOST }}
7984

8085
#-----------------------------------------------------------------------------
81-
# @eslint/compat
86+
# @eslint/migrate-config
8287
#-----------------------------------------------------------------------------
8388

84-
- name: Publish @eslint/compat package to npm
85-
run: npm publish -w packages/compat
86-
if: ${{ steps.release.outputs['packages/compat--release_created'] }}
89+
- name: Publish @eslint/migrate-config package to npm
90+
run: npm publish -w packages/migrate-config
91+
if: ${{ steps.release.outputs['packages/migrate-config--release_created'] }}
8792
env:
8893
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
8994

90-
- name: Publish @eslint/compat package to JSR
91-
run: |
92-
npm run build --if-present
93-
npx jsr publish
94-
working-directory: packages/compat
95-
if: ${{ steps.release.outputs['packages/compat--release_created'] }}
95+
# NOTE: No JSR package because JSR doesn't support CLIs
9696

9797
- name: Tweet Release Announcement
98-
run: npx @humanwhocodes/tweet "eslint/compat v${{ steps.release.outputs['packages/compat--major'] }}.${{ steps.release.outputs['packages/compat--minor'] }}.${{ steps.release.outputs['packages/compat--patch'] }} has been released!\n\n${{ github.event.repository.html_url }}/releases/tag/${{ steps.release.outputs['packages/compat--tag_name'] }}"
99-
if: ${{ steps.release.outputs['packages/compat--release_created'] }}
98+
run: npx @humanwhocodes/tweet "eslint/migrate-config v${{ steps.release.outputs['packages/migrate-config--major'] }}.${{ steps.release.outputs['packages/migrate-config--minor'] }}.${{ steps.release.outputs['packages/migrate-config--patch'] }} has been released!\n\n${{ github.event.repository.html_url }}/releases/tag/${{ steps.release.outputs['packages/migrate-config--tag_name'] }}"
99+
if: ${{ steps.release.outputs['packages/migrate-config--release_created'] }}
100100
env:
101101
TWITTER_CONSUMER_KEY: ${{ secrets.TWITTER_CONSUMER_KEY }}
102102
TWITTER_CONSUMER_SECRET: ${{ secrets.TWITTER_CONSUMER_SECRET }}
103103
TWITTER_ACCESS_TOKEN_KEY: ${{ secrets.TWITTER_ACCESS_TOKEN_KEY }}
104104
TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }}
105105

106106
- name: Toot Release Announcement
107-
run: npx @humanwhocodes/toot "eslint/compat v${{ steps.release.outputs['packages/compat--major'] }}.${{ steps.release.outputs['packages/compat--minor'] }}.${{ steps.release.outputs['packages/compat--patch'] }} has been released!\n\n${{ github.event.repository.html_url }}/releases/tag/${{ steps.release.outputs['packages/compat--tag_name'] }}"
108-
if: ${{ steps.release.outputs['packages/compat--release_created'] }}
107+
run: npx @humanwhocodes/toot "eslint/migrate-config v${{ steps.release.outputs['packages/migrate-config--major'] }}.${{ steps.release.outputs['packages/migrate-config--minor'] }}.${{ steps.release.outputs['packages/migrate-config--patch'] }} has been released!\n\n${{ github.event.repository.html_url }}/releases/tag/${{ steps.release.outputs['packages/migrate-config--tag_name'] }}"
108+
if: ${{ steps.release.outputs['packages/migrate-config--release_created'] }}
109109
env:
110110
MASTODON_ACCESS_TOKEN: ${{ secrets.MASTODON_ACCESS_TOKEN }}
111111
MASTODON_HOST: ${{ secrets.MASTODON_HOST }}

packages/compat/README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +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
3637

3738
### Fixing Rules
3839

@@ -142,6 +143,46 @@ module.exports = [
142143
];
143144
```
144145

146+
### Including Ignore Files
147+
148+
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. For example:
149+
150+
```js
151+
// eslint.config.js - ESM example
152+
import { includeIgnoreFile } from "@eslint/compat";
153+
import path from "node:path";
154+
import { fileURLToPath } from "node:url";
155+
156+
const __filename = fileURLToPath(import.meta.url);
157+
const __dirname = path.dirname(__filename);
158+
const gitignorePath = path.resolve(__dirname, ".gitignore");
159+
160+
export default [
161+
includeIgnoreFile(gitignorePath),
162+
{
163+
// your overrides
164+
},
165+
];
166+
```
167+
168+
Or in CommonJS:
169+
170+
```js
171+
// eslint.config.js - CommonJS example
172+
const { includeIgnoreFile } = require("@eslint/compat");
173+
const path = require("node:path");
174+
const gitignorePath = path.resolve(__dirname, ".gitignore");
175+
176+
module.exports = [
177+
includeIgnoreFile(gitignorePath),
178+
{
179+
// your overrides
180+
},
181+
];
182+
```
183+
184+
**Limitation:** This works without modification when the ignore file is in the same directory as your config file. If the ignore file is in a different directory, you may need to modify the patterns manually.
185+
145186
## License
146187
147188
Apache 2.0

packages/compat/src/fixup-rules.js

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
/**
2-
* @fileoverview Object Schema
2+
* @filedescription Functions to fix up rules to provide missing methods on the `context` object.
3+
* @author Nicholas C. Zakas
34
*/
45

5-
//-----------------------------------------------------------------------------
6-
// Imports
7-
//-----------------------------------------------------------------------------
8-
96
//-----------------------------------------------------------------------------
107
// Types
118
//-----------------------------------------------------------------------------

packages/compat/src/ignore-file.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/**
2+
* @fileoverview Ignore file utilities for the compat package.
3+
* @author Nicholas C. Zakas
4+
*/
5+
6+
//-----------------------------------------------------------------------------
7+
// Imports
8+
//-----------------------------------------------------------------------------
9+
10+
import fs from "node:fs";
11+
import path from "node:path";
12+
13+
//-----------------------------------------------------------------------------
14+
// Types
15+
//-----------------------------------------------------------------------------
16+
17+
/** @typedef {import("eslint").Linter.FlatConfig} FlatConfig */
18+
19+
//-----------------------------------------------------------------------------
20+
// Exports
21+
//-----------------------------------------------------------------------------
22+
23+
/**
24+
* Converts an ESLint ignore pattern to a minimatch pattern.
25+
* @param {string} pattern The .eslintignore or .gitignore pattern to convert.
26+
* @returns {string} The converted pattern.
27+
*/
28+
export function convertIgnorePatternToMinimatch(pattern) {
29+
const isNegated = pattern.startsWith("!");
30+
const negatedPrefix = isNegated ? "!" : "";
31+
const patternToTest = (isNegated ? pattern.slice(1) : pattern).trimEnd();
32+
33+
// special cases
34+
if (["", "**", "/**", "**/"].includes(patternToTest)) {
35+
return `${negatedPrefix}${patternToTest}`;
36+
}
37+
38+
const firstIndexOfSlash = patternToTest.indexOf("/");
39+
40+
const matchEverywherePrefix =
41+
firstIndexOfSlash < 0 || firstIndexOfSlash === patternToTest.length - 1
42+
? "**/"
43+
: "";
44+
45+
const patternWithoutLeadingSlash =
46+
firstIndexOfSlash === 0 ? patternToTest.slice(1) : patternToTest;
47+
48+
const matchInsideSuffix = patternToTest.endsWith("/**") ? "/*" : "";
49+
50+
return `${negatedPrefix}${matchEverywherePrefix}${patternWithoutLeadingSlash}${matchInsideSuffix}`;
51+
}
52+
53+
/**
54+
* Reads an ignore file and returns an object with the ignore patterns.
55+
* @param {string} ignoreFilePath The absolute path to the ignore file.
56+
* @returns {FlatConfig} An object with an `ignores` property that is an array of ignore patterns.
57+
* @throws {Error} If the ignore file path is not an absolute path.
58+
*/
59+
export function includeIgnoreFile(ignoreFilePath) {
60+
if (!path.isAbsolute(ignoreFilePath)) {
61+
throw new Error("The ignore file location must be an absolute path.");
62+
}
63+
64+
const ignoreFile = fs.readFileSync(ignoreFilePath, "utf8");
65+
const lines = ignoreFile.split(/\r?\n/u);
66+
67+
return {
68+
name: "Imported .gitignore patterns",
69+
ignores: lines
70+
.map(line => line.trim())
71+
.filter(line => line && !line.startsWith("#"))
72+
.map(convertIgnorePatternToMinimatch),
73+
};
74+
}

packages/compat/src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
*/
44

55
export * from "./fixup-rules.js";
6+
export * 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/**

packages/compat/tests/ignore-file.js

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/**
2+
* @filedescription Fixup tests
3+
*/
4+
5+
//-----------------------------------------------------------------------------
6+
// Imports
7+
//-----------------------------------------------------------------------------
8+
9+
import assert from "node:assert";
10+
import {
11+
includeIgnoreFile,
12+
convertIgnorePatternToMinimatch,
13+
} from "../src/ignore-file.js";
14+
import { fileURLToPath } from "node:url";
15+
16+
//-----------------------------------------------------------------------------
17+
// Tests
18+
//-----------------------------------------------------------------------------
19+
20+
describe("@eslint/compat", () => {
21+
describe("convertIgnorePatternToMinimatch", () => {
22+
const tests = [
23+
["", ""],
24+
["**", "**"],
25+
["/**", "/**"],
26+
["**/", "**/"],
27+
["src/", "**/src/"],
28+
["src", "**/src"],
29+
["src/**", "src/**/*"],
30+
["!src/", "!**/src/"],
31+
["!src", "!**/src"],
32+
["!src/**", "!src/**/*"],
33+
["*/foo.js", "*/foo.js"],
34+
["*/foo.js/", "*/foo.js/"],
35+
];
36+
37+
tests.forEach(([pattern, expected]) => {
38+
it(`should convert "${pattern}" to "${expected}"`, () => {
39+
assert.strictEqual(
40+
convertIgnorePatternToMinimatch(pattern),
41+
expected,
42+
);
43+
});
44+
});
45+
});
46+
47+
describe("includeIgnoreFile", () => {
48+
it("should throw an error when a relative path is passed", () => {
49+
const ignoreFilePath =
50+
"../tests/fixtures/ignore-files/gitignore1.txt";
51+
assert.throws(() => {
52+
includeIgnoreFile(ignoreFilePath);
53+
}, /The ignore file location must be an absolute path./u);
54+
});
55+
56+
it("should return an object with an `ignores` property", () => {
57+
const ignoreFilePath = fileURLToPath(
58+
new URL(
59+
"../tests/fixtures/ignore-files/gitignore1.txt",
60+
import.meta.url,
61+
),
62+
);
63+
const result = includeIgnoreFile(ignoreFilePath);
64+
assert.deepStrictEqual(result, {
65+
name: "Imported .gitignore patterns",
66+
ignores: [
67+
"**/node_modules",
68+
"!fixtures/node_modules",
69+
"dist",
70+
"**/*.log",
71+
"**/.cache/",
72+
".vuepress/dist",
73+
"*/foo.js",
74+
"dir/**/*",
75+
],
76+
});
77+
});
78+
});
79+
});

packages/migrate-config/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
4747
},
4848
"dependencies": {
49+
"@eslint/compat": "^1.0.3",
4950
"@eslint/eslintrc": "^3.1.0",
5051
"camelcase": "^8.0.0",
5152
"recast": "^0.23.7"

packages/migrate-config/src/migrate-config.js

Lines changed: 2 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import * as recast from "recast";
1111
import { Legacy } from "@eslint/eslintrc";
1212
import camelCase from "camelcase";
1313
import pluginsNeedingCompat from "./compat-plugins.js";
14+
import { convertIgnorePatternToMinimatch } from "@eslint/compat";
1415

1516
//-----------------------------------------------------------------------------
1617
// Types
@@ -121,36 +122,6 @@ function getParserVariableName(parser) {
121122
return "parser";
122123
}
123124

124-
/**
125-
* Converts an ESLint ignore pattern to a minimatch pattern.
126-
* @param {string} pattern The .eslintignore pattern to convert.
127-
* @returns {string} The converted pattern.
128-
*/
129-
function convertESLintIgnoreToMinimatch(pattern) {
130-
const isNegated = pattern.startsWith("!");
131-
const negatedPrefix = isNegated ? "!" : "";
132-
const patternToTest = (isNegated ? pattern.slice(1) : pattern).trimEnd();
133-
134-
// special cases
135-
if (["", "**", "/**", "**/"].includes(patternToTest)) {
136-
return `${negatedPrefix}${patternToTest}`;
137-
}
138-
139-
const firstIndexOfSlash = patternToTest.indexOf("/");
140-
141-
const matchEverywherePrefix =
142-
firstIndexOfSlash < 0 || firstIndexOfSlash === patternToTest.length - 1
143-
? "**/"
144-
: "";
145-
146-
const patternWithoutLeadingSlash =
147-
firstIndexOfSlash === 0 ? patternToTest.slice(1) : patternToTest;
148-
149-
const matchInsideSuffix = patternToTest.endsWith("/**") ? "/*" : "";
150-
151-
return `${negatedPrefix}${matchEverywherePrefix}${patternWithoutLeadingSlash}${matchInsideSuffix}`;
152-
}
153-
154125
// cache for plugins needing compat
155126
const pluginsNeedingCompatCache = new Set(pluginsNeedingCompat);
156127

@@ -609,7 +580,7 @@ function createGlobalIgnores(config) {
609580
: [config.ignorePatterns];
610581
const ignorePatternsArray = b.arrayExpression(
611582
ignorePatterns.map(pattern =>
612-
b.literal(convertESLintIgnoreToMinimatch(pattern)),
583+
b.literal(convertIgnorePatternToMinimatch(pattern)),
613584
),
614585
);
615586
return b.objectExpression([

0 commit comments

Comments
 (0)