Skip to content

Commit

Permalink
feat: Add includeIgnoreFile() method (#47)
Browse files Browse the repository at this point in the history
Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com>
  • Loading branch information
nzakas and mdjermanovic authored Jun 10, 2024
1 parent 10d8200 commit b5f74ed
Show file tree
Hide file tree
Showing 9 changed files with 240 additions and 59 deletions.
46 changes: 23 additions & 23 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,62 +50,62 @@ jobs:
#-----------------------------------------------------------------------------

#-----------------------------------------------------------------------------
# @eslint/migrate-config
# @eslint/compat
#-----------------------------------------------------------------------------

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

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

- name: Tweet Release Announcement
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'] }}"
if: ${{ steps.release.outputs['packages/migrate-config--release_created'] }}
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'] }}"
if: ${{ steps.release.outputs['packages/compat--release_created'] }}
env:
TWITTER_CONSUMER_KEY: ${{ secrets.TWITTER_CONSUMER_KEY }}
TWITTER_CONSUMER_SECRET: ${{ secrets.TWITTER_CONSUMER_SECRET }}
TWITTER_ACCESS_TOKEN_KEY: ${{ secrets.TWITTER_ACCESS_TOKEN_KEY }}
TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }}

- name: Toot Release Announcement
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'] }}"
if: ${{ steps.release.outputs['packages/migrate-config--release_created'] }}
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'] }}"
if: ${{ steps.release.outputs['packages/compat--release_created'] }}
env:
MASTODON_ACCESS_TOKEN: ${{ secrets.MASTODON_ACCESS_TOKEN }}
MASTODON_HOST: ${{ secrets.MASTODON_HOST }}

#-----------------------------------------------------------------------------
# @eslint/compat
# @eslint/migrate-config
#-----------------------------------------------------------------------------

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

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

- name: Tweet Release Announcement
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'] }}"
if: ${{ steps.release.outputs['packages/compat--release_created'] }}
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'] }}"
if: ${{ steps.release.outputs['packages/migrate-config--release_created'] }}
env:
TWITTER_CONSUMER_KEY: ${{ secrets.TWITTER_CONSUMER_KEY }}
TWITTER_CONSUMER_SECRET: ${{ secrets.TWITTER_CONSUMER_SECRET }}
TWITTER_ACCESS_TOKEN_KEY: ${{ secrets.TWITTER_ACCESS_TOKEN_KEY }}
TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }}

- name: Toot Release Announcement
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'] }}"
if: ${{ steps.release.outputs['packages/compat--release_created'] }}
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'] }}"
if: ${{ steps.release.outputs['packages/migrate-config--release_created'] }}
env:
MASTODON_ACCESS_TOKEN: ${{ secrets.MASTODON_ACCESS_TOKEN }}
MASTODON_HOST: ${{ secrets.MASTODON_HOST }}
Expand Down
41 changes: 41 additions & 0 deletions packages/compat/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ This package exports the following functions in both ESM and CommonJS format:
- `fixupRule(rule)` - wraps the given rule in a compatibility layer and returns the result
- `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
- `fixupConfigRules(configs)` - wraps all plugins found in an array of config objects using `fixupPluginRules()`
- `includeIgnoreFile(path)` - reads an ignore file (like `.gitignore`) and converts the patterns into the correct format for the config file

### Fixing Rules

Expand Down Expand Up @@ -142,6 +143,46 @@ module.exports = [
];
```

### Including Ignore Files

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:

```js
// eslint.config.js - ESM example
import { includeIgnoreFile } from "@eslint/compat";
import path from "node:path";
import { fileURLToPath } from "node:url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const gitignorePath = path.resolve(__dirname, ".gitignore");

export default [
includeIgnoreFile(gitignorePath),
{
// your overrides
},
];
```
Or in CommonJS:
```js
// eslint.config.js - CommonJS example
const { includeIgnoreFile } = require("@eslint/compat");
const path = require("node:path");
const gitignorePath = path.resolve(__dirname, ".gitignore");

module.exports = [
includeIgnoreFile(gitignorePath),
{
// your overrides
},
];
```
**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.
## License
Apache 2.0
7 changes: 2 additions & 5 deletions packages/compat/src/fixup-rules.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
/**
* @fileoverview Object Schema
* @filedescription Functions to fix up rules to provide missing methods on the `context` object.
* @author Nicholas C. Zakas
*/

//-----------------------------------------------------------------------------
// Imports
//-----------------------------------------------------------------------------

//-----------------------------------------------------------------------------
// Types
//-----------------------------------------------------------------------------
Expand Down
74 changes: 74 additions & 0 deletions packages/compat/src/ignore-file.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* @fileoverview Ignore file utilities for the compat package.
* @author Nicholas C. Zakas
*/

//-----------------------------------------------------------------------------
// Imports
//-----------------------------------------------------------------------------

import fs from "node:fs";
import path from "node:path";

//-----------------------------------------------------------------------------
// Types
//-----------------------------------------------------------------------------

/** @typedef {import("eslint").Linter.FlatConfig} FlatConfig */

//-----------------------------------------------------------------------------
// Exports
//-----------------------------------------------------------------------------

/**
* Converts an ESLint ignore pattern to a minimatch pattern.
* @param {string} pattern The .eslintignore or .gitignore pattern to convert.
* @returns {string} The converted pattern.
*/
export function convertIgnorePatternToMinimatch(pattern) {
const isNegated = pattern.startsWith("!");
const negatedPrefix = isNegated ? "!" : "";
const patternToTest = (isNegated ? pattern.slice(1) : pattern).trimEnd();

// special cases
if (["", "**", "/**", "**/"].includes(patternToTest)) {
return `${negatedPrefix}${patternToTest}`;
}

const firstIndexOfSlash = patternToTest.indexOf("/");

const matchEverywherePrefix =
firstIndexOfSlash < 0 || firstIndexOfSlash === patternToTest.length - 1
? "**/"
: "";

const patternWithoutLeadingSlash =
firstIndexOfSlash === 0 ? patternToTest.slice(1) : patternToTest;

const matchInsideSuffix = patternToTest.endsWith("/**") ? "/*" : "";

return `${negatedPrefix}${matchEverywherePrefix}${patternWithoutLeadingSlash}${matchInsideSuffix}`;
}

/**
* Reads an ignore file and returns an object with the ignore patterns.
* @param {string} ignoreFilePath The absolute path to the ignore file.
* @returns {FlatConfig} An object with an `ignores` property that is an array of ignore patterns.
* @throws {Error} If the ignore file path is not an absolute path.
*/
export function includeIgnoreFile(ignoreFilePath) {
if (!path.isAbsolute(ignoreFilePath)) {
throw new Error("The ignore file location must be an absolute path.");
}

const ignoreFile = fs.readFileSync(ignoreFilePath, "utf8");
const lines = ignoreFile.split(/\r?\n/u);

return {
name: "Imported .gitignore patterns",
ignores: lines
.map(line => line.trim())
.filter(line => line && !line.startsWith("#"))
.map(convertIgnorePatternToMinimatch),
};
}
1 change: 1 addition & 0 deletions packages/compat/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
*/

export * from "./fixup-rules.js";
export * from "./ignore-file.js";
17 changes: 17 additions & 0 deletions packages/compat/tests/fixtures/ignore-files/gitignore1.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Node.js
node_modules
!/fixtures/node_modules
/dist

# Logs
*.log

# Gatsby files
.cache/

# vuepress build output
.vuepress/dist

# other
*/foo.js
dir/**
79 changes: 79 additions & 0 deletions packages/compat/tests/ignore-file.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* @filedescription Fixup tests
*/

//-----------------------------------------------------------------------------
// Imports
//-----------------------------------------------------------------------------

import assert from "node:assert";
import {
includeIgnoreFile,
convertIgnorePatternToMinimatch,
} from "../src/ignore-file.js";
import { fileURLToPath } from "node:url";

//-----------------------------------------------------------------------------
// Tests
//-----------------------------------------------------------------------------

describe("@eslint/compat", () => {
describe("convertIgnorePatternToMinimatch", () => {
const tests = [
["", ""],
["**", "**"],
["/**", "/**"],
["**/", "**/"],
["src/", "**/src/"],
["src", "**/src"],
["src/**", "src/**/*"],
["!src/", "!**/src/"],
["!src", "!**/src"],
["!src/**", "!src/**/*"],
["*/foo.js", "*/foo.js"],
["*/foo.js/", "*/foo.js/"],
];

tests.forEach(([pattern, expected]) => {
it(`should convert "${pattern}" to "${expected}"`, () => {
assert.strictEqual(
convertIgnorePatternToMinimatch(pattern),
expected,
);
});
});
});

describe("includeIgnoreFile", () => {
it("should throw an error when a relative path is passed", () => {
const ignoreFilePath =
"../tests/fixtures/ignore-files/gitignore1.txt";
assert.throws(() => {
includeIgnoreFile(ignoreFilePath);
}, /The ignore file location must be an absolute path./u);
});

it("should return an object with an `ignores` property", () => {
const ignoreFilePath = fileURLToPath(
new URL(
"../tests/fixtures/ignore-files/gitignore1.txt",
import.meta.url,
),
);
const result = includeIgnoreFile(ignoreFilePath);
assert.deepStrictEqual(result, {
name: "Imported .gitignore patterns",
ignores: [
"**/node_modules",
"!fixtures/node_modules",
"dist",
"**/*.log",
"**/.cache/",
".vuepress/dist",
"*/foo.js",
"dir/**/*",
],
});
});
});
});
1 change: 1 addition & 0 deletions packages/migrate-config/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"dependencies": {
"@eslint/compat": "^1.0.3",
"@eslint/eslintrc": "^3.1.0",
"camelcase": "^8.0.0",
"recast": "^0.23.7"
Expand Down
33 changes: 2 additions & 31 deletions packages/migrate-config/src/migrate-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import * as recast from "recast";
import { Legacy } from "@eslint/eslintrc";
import camelCase from "camelcase";
import pluginsNeedingCompat from "./compat-plugins.js";
import { convertIgnorePatternToMinimatch } from "@eslint/compat";

//-----------------------------------------------------------------------------
// Types
Expand Down Expand Up @@ -121,36 +122,6 @@ function getParserVariableName(parser) {
return "parser";
}

/**
* Converts an ESLint ignore pattern to a minimatch pattern.
* @param {string} pattern The .eslintignore pattern to convert.
* @returns {string} The converted pattern.
*/
function convertESLintIgnoreToMinimatch(pattern) {
const isNegated = pattern.startsWith("!");
const negatedPrefix = isNegated ? "!" : "";
const patternToTest = (isNegated ? pattern.slice(1) : pattern).trimEnd();

// special cases
if (["", "**", "/**", "**/"].includes(patternToTest)) {
return `${negatedPrefix}${patternToTest}`;
}

const firstIndexOfSlash = patternToTest.indexOf("/");

const matchEverywherePrefix =
firstIndexOfSlash < 0 || firstIndexOfSlash === patternToTest.length - 1
? "**/"
: "";

const patternWithoutLeadingSlash =
firstIndexOfSlash === 0 ? patternToTest.slice(1) : patternToTest;

const matchInsideSuffix = patternToTest.endsWith("/**") ? "/*" : "";

return `${negatedPrefix}${matchEverywherePrefix}${patternWithoutLeadingSlash}${matchInsideSuffix}`;
}

// cache for plugins needing compat
const pluginsNeedingCompatCache = new Set(pluginsNeedingCompat);

Expand Down Expand Up @@ -609,7 +580,7 @@ function createGlobalIgnores(config) {
: [config.ignorePatterns];
const ignorePatternsArray = b.arrayExpression(
ignorePatterns.map(pattern =>
b.literal(convertESLintIgnoreToMinimatch(pattern)),
b.literal(convertIgnorePatternToMinimatch(pattern)),
),
);
return b.objectExpression([
Expand Down

0 comments on commit b5f74ed

Please sign in to comment.