Skip to content

Commit 19cadbe

Browse files
feat: support flat config (#238)
* feat: support ESLint's flat config * docs(readme): add new `configType` option to readme * ci: remove eslint 7x * test: skip test on node versions lt 20 --------- Co-authored-by: Ricardo Gobbo de Souza <[email protected]>
1 parent 445389c commit 19cadbe

File tree

9 files changed

+134
-23
lines changed

9 files changed

+134
-23
lines changed

.github/workflows/nodejs.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ jobs:
6060
matrix:
6161
os: [ubuntu-latest, windows-latest, macos-latest]
6262
node-version: [14.x, 16.x, 18.x, 20.x]
63-
eslint-version: [7.x, 8.x]
63+
eslint-version: [8.x]
6464
webpack-version: [latest]
6565

6666
runs-on: ${{ matrix.os }}

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,25 @@ type cacheLocation = string;
106106

107107
Specify the path to the cache location. Can be a file or a directory.
108108

109+
### `configType`
110+
111+
- Type:
112+
113+
```ts
114+
type configType = "flat" | "eslintrc";
115+
```
116+
117+
- Default: `eslintrc`
118+
119+
Specify the type of configuration to use with ESLint.
120+
- `eslintrc` is the classic configuration format available in most ESLint versions.
121+
- `flat` is the new format introduced in ESLint 8.21.0.
122+
123+
The new configuration format is explained in its [own documentation](https://eslint.org/docs/latest/use/configure/configuration-files-new).
124+
125+
> This configuration format being considered as experimental, it is not exported in the main ESLint module in ESLint 8.
126+
> You need to set your `eslintPath` to `eslint/use-at-your-own-risk` for this config format to work.
127+
109128
### `context`
110129

111130
- Type:

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"fix:js": "npm run lint:js -- --fix",
3434
"fix:prettier": "npm run lint:prettier -- --write",
3535
"fix": "npm-run-all -l fix:js fix:prettier",
36-
"test:only": "cross-env NODE_ENV=test jest --testTimeout=60000",
36+
"test:only": "cross-env NODE_OPTIONS=--experimental-vm-modules NODE_ENV=test jest --testTimeout=60000",
3737
"test:watch": "npm run test:only -- --watch",
3838
"test:coverage": "npm run test:only -- --collectCoverageFrom=\"src/**/*.js\" --coverage",
3939
"pretest": "npm run lint",

src/getESLint.js

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ const { cpus } = require('os');
22

33
const { Worker: JestWorker } = require('jest-worker');
44

5+
// @ts-ignore
6+
const { setup, lintFiles } = require('./worker');
57
const { getESLintOptions } = require('./options');
68
const { jsonStringifyReplacerSortKeys } = require('./utils');
79

@@ -13,7 +15,7 @@ const cache = {};
1315
/** @typedef {import('./options').Options} Options */
1416
/** @typedef {() => Promise<void>} AsyncTask */
1517
/** @typedef {(files: string|string[]) => Promise<LintResult[]>} LintTask */
16-
/** @typedef {{threads: number, ESLint: ESLint, eslint: ESLint, lintFiles: LintTask, cleanup: AsyncTask}} Linter */
18+
/** @typedef {{threads: number, eslint: ESLint, lintFiles: LintTask, cleanup: AsyncTask}} Linter */
1719
/** @typedef {JestWorker & {lintFiles: LintTask}} Worker */
1820

1921
/**
@@ -22,24 +24,16 @@ const cache = {};
2224
*/
2325
function loadESLint(options) {
2426
const { eslintPath } = options;
25-
26-
const { ESLint } = require(eslintPath || 'eslint');
27-
28-
// Filter out loader options before passing the options to ESLint.
29-
const eslint = new ESLint(getESLintOptions(options));
27+
const eslint = setup({
28+
eslintPath,
29+
configType: options.configType,
30+
eslintOptions: getESLintOptions(options),
31+
});
3032

3133
return {
3234
threads: 1,
33-
ESLint,
35+
lintFiles,
3436
eslint,
35-
lintFiles: async (files) => {
36-
const results = await eslint.lintFiles(files);
37-
// istanbul ignore else
38-
if (options.fix) {
39-
await ESLint.outputFixes(results);
40-
}
41-
return results;
42-
},
4337
// no-op for non-threaded
4438
cleanup: async () => {},
4539
};
@@ -58,7 +52,13 @@ function loadESLintThreaded(key, poolSize, options) {
5852
const workerOptions = {
5953
enableWorkerThreads: true,
6054
numWorkers: poolSize,
61-
setupArgs: [{ eslintPath, eslintOptions: getESLintOptions(options) }],
55+
setupArgs: [
56+
{
57+
eslintPath,
58+
configType: options.configType,
59+
eslintOptions: getESLintOptions(options),
60+
},
61+
],
6262
};
6363

6464
const local = loadESLint(options);

src/options.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const schema = require('./options.json');
3737
* @property {OutputReport=} outputReport
3838
* @property {number|boolean=} threads
3939
* @property {RegExp|RegExp[]=} resourceQueryExclude
40+
* @property {string=} configType
4041
*/
4142

4243
/** @typedef {PluginOptions & ESLintOptions} Options */
@@ -84,6 +85,11 @@ function getESLintOptions(loaderOptions) {
8485
delete eslintOptions[option];
8586
}
8687

88+
// Some options aren't available in flat mode
89+
if (loaderOptions.configType === 'flat') {
90+
delete eslintOptions.extensions;
91+
}
92+
8793
return eslintOptions;
8894
}
8995

src/options.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
"type": "object",
33
"additionalProperties": true,
44
"properties": {
5+
"configType": {
6+
"description": "Enable flat config by setting this value to `flat`.",
7+
"type": "string"
8+
},
59
"context": {
610
"description": "A string indicating the root of your files.",
711
"type": "string"

src/worker.js

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
/** @typedef {import('eslint').ESLint} ESLint */
22
/** @typedef {import('eslint').ESLint.Options} ESLintOptions */
3+
/** @typedef {import('eslint').ESLint.LintResult} LintResult */
34

45
Object.assign(module.exports, {
56
lintFiles,
67
setup,
78
});
89

9-
/** @type {{ new (arg0: import("eslint").ESLint.Options): import("eslint").ESLint; outputFixes: (arg0: import("eslint").ESLint.LintResult[]) => any; }} */
10+
/** @type {{ new (arg0: ESLintOptions): ESLint; outputFixes: (arg0: LintResult[]) => any; }} */
1011
let ESLint;
1112

1213
/** @type {ESLint} */
@@ -18,20 +19,44 @@ let fix;
1819
/**
1920
* @typedef {object} setupOptions
2021
* @property {string=} eslintPath - import path of eslint
21-
* @property {ESLintOptions=} eslintOptions - linter options
22+
* @property {string=} configType
23+
* @property {ESLintOptions} eslintOptions - linter options
2224
*
2325
* @param {setupOptions} arg0 - setup worker
2426
*/
25-
function setup({ eslintPath, eslintOptions = {} }) {
27+
function setup({ eslintPath, configType, eslintOptions }) {
2628
fix = !!(eslintOptions && eslintOptions.fix);
27-
({ ESLint } = require(eslintPath || 'eslint'));
28-
eslint = new ESLint(eslintOptions);
29+
const eslintModule = require(eslintPath || 'eslint');
30+
31+
let FlatESLint;
32+
33+
if (eslintModule.LegacyESLint) {
34+
ESLint = eslintModule.LegacyESLint;
35+
({ FlatESLint } = eslintModule);
36+
} else {
37+
({ ESLint } = eslintModule);
38+
39+
if (configType === 'flat') {
40+
throw new Error(
41+
"Couldn't find FlatESLint, you might need to set eslintPath to 'eslint/use-at-your-own-risk'",
42+
);
43+
}
44+
}
45+
46+
if (configType === 'flat') {
47+
eslint = new FlatESLint(eslintOptions);
48+
} else {
49+
eslint = new ESLint(eslintOptions);
50+
}
51+
52+
return eslint;
2953
}
3054

3155
/**
3256
* @param {string | string[]} files
3357
*/
3458
async function lintFiles(files) {
59+
/** @type {LintResult[]} */
3560
const result = await eslint.lintFiles(files);
3661
// if enabled, use eslint autofixing where possible
3762
if (fix) {

test/fixtures/flat-config.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
2+
module.exports = [
3+
{
4+
files: ["*.js"],
5+
rules: {}
6+
}
7+
];

test/flat-config.test.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { join } from 'path';
2+
3+
import pack from './utils/pack';
4+
5+
describe('succeed on flat-configuration', () => {
6+
it('cannot load FlatESLint class on default ESLint module', (done) => {
7+
const overrideConfigFile = join(__dirname, 'fixtures', 'flat-config.js');
8+
const compiler = pack('full-of-problems', {
9+
configType: 'flat',
10+
overrideConfigFile,
11+
threads: 1,
12+
});
13+
14+
compiler.run((err, stats) => {
15+
expect(err).toBeNull();
16+
const { errors } = stats.compilation;
17+
18+
expect(stats.hasErrors()).toBe(true);
19+
expect(errors).toHaveLength(1);
20+
expect(errors[0].message).toMatch(
21+
/Couldn't find FlatESLint, you might need to set eslintPath to 'eslint\/use-at-your-own-risk'/i,
22+
);
23+
done();
24+
});
25+
});
26+
27+
(process.version.match(/^v(\d+\.\d+)/)[1] >= 20 ? it : it.skip)('finds errors on files', (done) => {
28+
const overrideConfigFile = join(__dirname, 'fixtures', 'flat-config.js');
29+
const compiler = pack('full-of-problems', {
30+
configType: 'flat',
31+
// needed for now
32+
eslintPath: 'eslint/use-at-your-own-risk',
33+
overrideConfigFile,
34+
threads: 1,
35+
});
36+
37+
compiler.run((err, stats) => {
38+
expect(err).toBeNull();
39+
const { errors } = stats.compilation;
40+
41+
expect(stats.hasErrors()).toBe(true);
42+
expect(errors).toHaveLength(1);
43+
expect(errors[0].message).toMatch(
44+
/full-of-problems\.js/i,
45+
);
46+
expect(stats.hasWarnings()).toBe(true);
47+
done();
48+
});
49+
});
50+
});

0 commit comments

Comments
 (0)