diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 157fe31..9fbac6c 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -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 }} diff --git a/README.md b/README.md index f6c7af4..d01559f 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/package.json b/package.json index ffdd16a..c49f972 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/getESLint.js b/src/getESLint.js index 74382e2..a5d5f57 100644 --- a/src/getESLint.js +++ b/src/getESLint.js @@ -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'); @@ -13,7 +15,7 @@ const cache = {}; /** @typedef {import('./options').Options} Options */ /** @typedef {() => Promise} AsyncTask */ /** @typedef {(files: string|string[]) => Promise} 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 */ /** @@ -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 () => {}, }; @@ -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); diff --git a/src/options.js b/src/options.js index 4d1fce6..7d153cd 100644 --- a/src/options.js +++ b/src/options.js @@ -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 */ @@ -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; } diff --git a/src/options.json b/src/options.json index caa39ff..c25be41 100644 --- a/src/options.json +++ b/src/options.json @@ -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" diff --git a/src/worker.js b/src/worker.js index 6e5f842..5ac65bb 100644 --- a/src/worker.js +++ b/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} */ @@ -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) { diff --git a/test/fixtures/flat-config.js b/test/fixtures/flat-config.js new file mode 100644 index 0000000..c213271 --- /dev/null +++ b/test/fixtures/flat-config.js @@ -0,0 +1,7 @@ + +module.exports = [ + { + files: ["*.js"], + rules: {} + } +]; diff --git a/test/flat-config.test.js b/test/flat-config.test.js new file mode 100644 index 0000000..6d2abd2 --- /dev/null +++ b/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(); + }); + }); +});