Skip to content

Commit

Permalink
feat: support flat config (#238)
Browse files Browse the repository at this point in the history
* 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 <ricardogobbosouza@yahoo.com.br>
  • Loading branch information
onigoetz and ricardogobbosouza committed Jan 2, 2024
1 parent 445389c commit 19cadbe
Show file tree
Hide file tree
Showing 9 changed files with 134 additions and 23 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/nodejs.yml
Expand Up @@ -60,7 +60,7 @@ jobs:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [14.x, 16.x, 18.x, 20.x]
eslint-version: [7.x, 8.x]
eslint-version: [8.x]
webpack-version: [latest]

runs-on: ${{ matrix.os }}
Expand Down
19 changes: 19 additions & 0 deletions README.md
Expand Up @@ -106,6 +106,25 @@ type cacheLocation = string;

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

### `configType`

- Type:

```ts
type configType = "flat" | "eslintrc";
```

- Default: `eslintrc`

Specify the type of configuration to use with ESLint.
- `eslintrc` is the classic configuration format available in most ESLint versions.
- `flat` is the new format introduced in ESLint 8.21.0.

The new configuration format is explained in its [own documentation](https://eslint.org/docs/latest/use/configure/configuration-files-new).

> This configuration format being considered as experimental, it is not exported in the main ESLint module in ESLint 8.
> You need to set your `eslintPath` to `eslint/use-at-your-own-risk` for this config format to work.
### `context`

- Type:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -33,7 +33,7 @@
"fix:js": "npm run lint:js -- --fix",
"fix:prettier": "npm run lint:prettier -- --write",
"fix": "npm-run-all -l fix:js fix:prettier",
"test:only": "cross-env NODE_ENV=test jest --testTimeout=60000",
"test:only": "cross-env NODE_OPTIONS=--experimental-vm-modules NODE_ENV=test jest --testTimeout=60000",
"test:watch": "npm run test:only -- --watch",
"test:coverage": "npm run test:only -- --collectCoverageFrom=\"src/**/*.js\" --coverage",
"pretest": "npm run lint",
Expand Down
32 changes: 16 additions & 16 deletions src/getESLint.js
Expand Up @@ -2,6 +2,8 @@ const { cpus } = require('os');

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

// @ts-ignore
const { setup, lintFiles } = require('./worker');
const { getESLintOptions } = require('./options');
const { jsonStringifyReplacerSortKeys } = require('./utils');

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

/**
Expand All @@ -22,24 +24,16 @@ const cache = {};
*/
function loadESLint(options) {
const { eslintPath } = options;

const { ESLint } = require(eslintPath || 'eslint');

// Filter out loader options before passing the options to ESLint.
const eslint = new ESLint(getESLintOptions(options));
const eslint = setup({
eslintPath,
configType: options.configType,
eslintOptions: getESLintOptions(options),
});

return {
threads: 1,
ESLint,
lintFiles,
eslint,
lintFiles: async (files) => {
const results = await eslint.lintFiles(files);
// istanbul ignore else
if (options.fix) {
await ESLint.outputFixes(results);
}
return results;
},
// no-op for non-threaded
cleanup: async () => {},
};
Expand All @@ -58,7 +52,13 @@ function loadESLintThreaded(key, poolSize, options) {
const workerOptions = {
enableWorkerThreads: true,
numWorkers: poolSize,
setupArgs: [{ eslintPath, eslintOptions: getESLintOptions(options) }],
setupArgs: [
{
eslintPath,
configType: options.configType,
eslintOptions: getESLintOptions(options),
},
],
};

const local = loadESLint(options);
Expand Down
6 changes: 6 additions & 0 deletions src/options.js
Expand Up @@ -37,6 +37,7 @@ const schema = require('./options.json');
* @property {OutputReport=} outputReport
* @property {number|boolean=} threads
* @property {RegExp|RegExp[]=} resourceQueryExclude
* @property {string=} configType
*/

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

// Some options aren't available in flat mode
if (loaderOptions.configType === 'flat') {
delete eslintOptions.extensions;
}

return eslintOptions;
}

Expand Down
4 changes: 4 additions & 0 deletions src/options.json
Expand Up @@ -2,6 +2,10 @@
"type": "object",
"additionalProperties": true,
"properties": {
"configType": {
"description": "Enable flat config by setting this value to `flat`.",
"type": "string"
},
"context": {
"description": "A string indicating the root of your files.",
"type": "string"
Expand Down
35 changes: 30 additions & 5 deletions src/worker.js
@@ -1,12 +1,13 @@
/** @typedef {import('eslint').ESLint} ESLint */
/** @typedef {import('eslint').ESLint.Options} ESLintOptions */
/** @typedef {import('eslint').ESLint.LintResult} LintResult */

Object.assign(module.exports, {
lintFiles,
setup,
});

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

/** @type {ESLint} */
Expand All @@ -18,20 +19,44 @@ let fix;
/**
* @typedef {object} setupOptions
* @property {string=} eslintPath - import path of eslint
* @property {ESLintOptions=} eslintOptions - linter options
* @property {string=} configType
* @property {ESLintOptions} eslintOptions - linter options
*
* @param {setupOptions} arg0 - setup worker
*/
function setup({ eslintPath, eslintOptions = {} }) {
function setup({ eslintPath, configType, eslintOptions }) {
fix = !!(eslintOptions && eslintOptions.fix);
({ ESLint } = require(eslintPath || 'eslint'));
eslint = new ESLint(eslintOptions);
const eslintModule = require(eslintPath || 'eslint');

let FlatESLint;

if (eslintModule.LegacyESLint) {
ESLint = eslintModule.LegacyESLint;
({ FlatESLint } = eslintModule);
} else {
({ ESLint } = eslintModule);

if (configType === 'flat') {
throw new Error(
"Couldn't find FlatESLint, you might need to set eslintPath to 'eslint/use-at-your-own-risk'",
);
}
}

if (configType === 'flat') {
eslint = new FlatESLint(eslintOptions);
} else {
eslint = new ESLint(eslintOptions);
}

return eslint;
}

/**
* @param {string | string[]} files
*/
async function lintFiles(files) {
/** @type {LintResult[]} */
const result = await eslint.lintFiles(files);
// if enabled, use eslint autofixing where possible
if (fix) {
Expand Down
7 changes: 7 additions & 0 deletions test/fixtures/flat-config.js
@@ -0,0 +1,7 @@

module.exports = [
{
files: ["*.js"],
rules: {}
}
];
50 changes: 50 additions & 0 deletions test/flat-config.test.js
@@ -0,0 +1,50 @@
import { join } from 'path';

import pack from './utils/pack';

describe('succeed on flat-configuration', () => {
it('cannot load FlatESLint class on default ESLint module', (done) => {
const overrideConfigFile = join(__dirname, 'fixtures', 'flat-config.js');
const compiler = pack('full-of-problems', {
configType: 'flat',
overrideConfigFile,
threads: 1,
});

compiler.run((err, stats) => {
expect(err).toBeNull();
const { errors } = stats.compilation;

expect(stats.hasErrors()).toBe(true);
expect(errors).toHaveLength(1);
expect(errors[0].message).toMatch(
/Couldn't find FlatESLint, you might need to set eslintPath to 'eslint\/use-at-your-own-risk'/i,
);
done();
});
});

(process.version.match(/^v(\d+\.\d+)/)[1] >= 20 ? it : it.skip)('finds errors on files', (done) => {
const overrideConfigFile = join(__dirname, 'fixtures', 'flat-config.js');
const compiler = pack('full-of-problems', {
configType: 'flat',
// needed for now
eslintPath: 'eslint/use-at-your-own-risk',
overrideConfigFile,
threads: 1,
});

compiler.run((err, stats) => {
expect(err).toBeNull();
const { errors } = stats.compilation;

expect(stats.hasErrors()).toBe(true);
expect(errors).toHaveLength(1);
expect(errors[0].message).toMatch(
/full-of-problems\.js/i,
);
expect(stats.hasWarnings()).toBe(true);
done();
});
});
});

0 comments on commit 19cadbe

Please sign in to comment.