From b8bd2ad7a229c353110580a616feba09699e2a0b Mon Sep 17 00:00:00 2001 From: Connor Prussin Date: Sat, 20 May 2023 09:16:44 -0700 Subject: [PATCH] Support eslint flat config --- .github/workflows/nodejs.yml | 2 +- .../flat-config/__eslint__/file.js | 3 + .../__fixtures__/flat-config/eslint.config.js | 7 ++ .../__fixtures__/flat-config/jest.config.js | 4 + .../__eslint__/file.js | 3 + .../eslint.config.js | 5 ++ .../jest-runner-eslint.config.js | 3 + .../jest.config.js | 4 + .../__snapshots__/flat-config.test.js.snap | 19 +++++ ...cy-config-at-eslint.config.js.test.js.snap | 22 +++++ integrationTests/flat-config.test.js | 10 +++ .../legacy-config-at-eslint.config.js.test.js | 12 +++ src/runner/runESLint.js | 83 ++++++++++++++++--- 13 files changed, 163 insertions(+), 14 deletions(-) create mode 100644 integrationTests/__fixtures__/flat-config/__eslint__/file.js create mode 100644 integrationTests/__fixtures__/flat-config/eslint.config.js create mode 100644 integrationTests/__fixtures__/flat-config/jest.config.js create mode 100644 integrationTests/__fixtures__/legacy-config-at-eslint.config.js/__eslint__/file.js create mode 100644 integrationTests/__fixtures__/legacy-config-at-eslint.config.js/eslint.config.js create mode 100644 integrationTests/__fixtures__/legacy-config-at-eslint.config.js/jest-runner-eslint.config.js create mode 100644 integrationTests/__fixtures__/legacy-config-at-eslint.config.js/jest.config.js create mode 100644 integrationTests/__snapshots__/flat-config.test.js.snap create mode 100644 integrationTests/__snapshots__/legacy-config-at-eslint.config.js.test.js.snap create mode 100644 integrationTests/flat-config.test.js create mode 100644 integrationTests/legacy-config-at-eslint.config.js.test.js diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index f20d8ea..8892899 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -19,7 +19,7 @@ jobs: fail-fast: false matrix: node-version: [12.x, 14.x, 16.x, 18.x, 19.x] - eslint-version: [7, 8] + eslint-version: [7, "8.40", 8] jest-version: [27, 28, 29] jest-watch-typeahead-version: [1, 2] exclude: diff --git a/integrationTests/__fixtures__/flat-config/__eslint__/file.js b/integrationTests/__fixtures__/flat-config/__eslint__/file.js new file mode 100644 index 0000000..7720ff4 --- /dev/null +++ b/integrationTests/__fixtures__/flat-config/__eslint__/file.js @@ -0,0 +1,3 @@ +const a = 1; + +console.log('a', a); diff --git a/integrationTests/__fixtures__/flat-config/eslint.config.js b/integrationTests/__fixtures__/flat-config/eslint.config.js new file mode 100644 index 0000000..2d743d4 --- /dev/null +++ b/integrationTests/__fixtures__/flat-config/eslint.config.js @@ -0,0 +1,7 @@ +module.exports = [ + { + rules: { + 'no-console': 'error', + }, + }, +]; diff --git a/integrationTests/__fixtures__/flat-config/jest.config.js b/integrationTests/__fixtures__/flat-config/jest.config.js new file mode 100644 index 0000000..94f5944 --- /dev/null +++ b/integrationTests/__fixtures__/flat-config/jest.config.js @@ -0,0 +1,4 @@ +module.exports = { + runner: '../../../', + testMatch: ['**/__eslint__/**/*.js'], +}; diff --git a/integrationTests/__fixtures__/legacy-config-at-eslint.config.js/__eslint__/file.js b/integrationTests/__fixtures__/legacy-config-at-eslint.config.js/__eslint__/file.js new file mode 100644 index 0000000..7720ff4 --- /dev/null +++ b/integrationTests/__fixtures__/legacy-config-at-eslint.config.js/__eslint__/file.js @@ -0,0 +1,3 @@ +const a = 1; + +console.log('a', a); diff --git a/integrationTests/__fixtures__/legacy-config-at-eslint.config.js/eslint.config.js b/integrationTests/__fixtures__/legacy-config-at-eslint.config.js/eslint.config.js new file mode 100644 index 0000000..c4f6207 --- /dev/null +++ b/integrationTests/__fixtures__/legacy-config-at-eslint.config.js/eslint.config.js @@ -0,0 +1,5 @@ +module.exports = { + rules: { + 'no-console': 'error', + }, +}; diff --git a/integrationTests/__fixtures__/legacy-config-at-eslint.config.js/jest-runner-eslint.config.js b/integrationTests/__fixtures__/legacy-config-at-eslint.config.js/jest-runner-eslint.config.js new file mode 100644 index 0000000..1dbf367 --- /dev/null +++ b/integrationTests/__fixtures__/legacy-config-at-eslint.config.js/jest-runner-eslint.config.js @@ -0,0 +1,3 @@ +module.exports = { + config: './eslint.config.js', +}; diff --git a/integrationTests/__fixtures__/legacy-config-at-eslint.config.js/jest.config.js b/integrationTests/__fixtures__/legacy-config-at-eslint.config.js/jest.config.js new file mode 100644 index 0000000..94f5944 --- /dev/null +++ b/integrationTests/__fixtures__/legacy-config-at-eslint.config.js/jest.config.js @@ -0,0 +1,4 @@ +module.exports = { + runner: '../../../', + testMatch: ['**/__eslint__/**/*.js'], +}; diff --git a/integrationTests/__snapshots__/flat-config.test.js.snap b/integrationTests/__snapshots__/flat-config.test.js.snap new file mode 100644 index 0000000..3e54162 --- /dev/null +++ b/integrationTests/__snapshots__/flat-config.test.js.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Works with the new flat config format 1`] = ` +"FAIL __eslint__/file.js + ✕ no-console + + +/mocked-path-to-jest-runner-mocha/integrationTests/__fixtures__/flat-config/__eslint__/file.js + 3:1 error Unexpected console statement no-console + +✖ 1 problem (1 error, 0 warnings) + +Test Suites: 1 failed, 1 total +Tests: 1 failed, 1 total +Snapshots: 0 total +Time: +Ran all test suites. +" +`; diff --git a/integrationTests/__snapshots__/legacy-config-at-eslint.config.js.test.js.snap b/integrationTests/__snapshots__/legacy-config-at-eslint.config.js.test.js.snap new file mode 100644 index 0000000..b4c905e --- /dev/null +++ b/integrationTests/__snapshots__/legacy-config-at-eslint.config.js.test.js.snap @@ -0,0 +1,22 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Does not try to use flat config on eslint versions that don't support it 1`] = ` +"PASS __eslint__/file.js + ● Console + + console.warn + + /mocked-path-to-jest-runner-mocha/integrationTests/__fixtures__/legacy-config-at-eslint.config.js/__eslint__/file.js + 3:1 warning Unexpected console statement no-console + + ✖ 1 problem (0 errors, 1 warning) + + ✓ ESLint + +Test Suites: 1 passed, 1 total +Tests: 1 passed, 1 total +Snapshots: 0 total +Time: +Ran all test suites. +" +`; diff --git a/integrationTests/flat-config.test.js b/integrationTests/flat-config.test.js new file mode 100644 index 0000000..55d716d --- /dev/null +++ b/integrationTests/flat-config.test.js @@ -0,0 +1,10 @@ +const { version } = require('eslint/package.json'); +const semver = require('semver'); +const runJest = require('./runJest'); + +(semver.satisfies(version, '>=8.41') ? it : it.skip)( + 'Works with the new flat config format', + async () => { + expect(await runJest('flat-config')).toMatchSnapshot(); + }, +); diff --git a/integrationTests/legacy-config-at-eslint.config.js.test.js b/integrationTests/legacy-config-at-eslint.config.js.test.js new file mode 100644 index 0000000..59fad05 --- /dev/null +++ b/integrationTests/legacy-config-at-eslint.config.js.test.js @@ -0,0 +1,12 @@ +const { version } = require('eslint/package.json'); +const semver = require('semver'); +const runJest = require('./runJest'); + +(semver.satisfies(version, '<8.41') ? it : it.skip)( + "Does not try to use flat config on eslint versions that don't support it", + async () => { + expect( + await runJest('legacy-config-at-eslint.config.js'), + ).toMatchSnapshot(); + }, +); diff --git a/src/runner/runESLint.js b/src/runner/runESLint.js index f94424f..144bb25 100644 --- a/src/runner/runESLint.js +++ b/src/runner/runESLint.js @@ -1,6 +1,32 @@ const { ESLint } = require('eslint'); const getESLintOptions = require('../utils/getESLintOptions'); +let FlatESLint; +let shouldUseFlatConfig; + +try { + // Use a dynamic require here rather than a global require because this + // import path does not exist in eslint v7 which this library still + // supports + // + // ESlint exposes the new FlatESLint API under `eslint/use-at-your-own-risk` by + // using it's [export configuration](https://tinyurl.com/2s45zh9b). However, + // the `import/no-unresolved` rule is [not aware of + // `exports`](https://tinyurl.com/469djpx3) and causes a false error here. So, + // let's ignore that rule for this import. + // + // eslint-disable-next-line global-require, import/no-unresolved + const eslintExperimental = require('eslint/use-at-your-own-risk'); + FlatESLint = eslintExperimental.FlatESLint; + shouldUseFlatConfig = eslintExperimental.shouldUseFlatConfig; +} catch { + /* no-op */ +} + +if (shouldUseFlatConfig === undefined) { + shouldUseFlatConfig = () => Promise.resolve(false); +} + /* * This function exists because there are issues with the `pass`, `skip`, and * `fail` functions from `create-jest-runner`: @@ -96,8 +122,41 @@ const getComputedFixValue = ({ fix, quiet, fixDryRun }) => { return undefined; }; +const getESLintConstructor = async () => { + if (await shouldUseFlatConfig()) { + return FlatESLint; + } + + return ESLint; +}; + +// Remove options that are not constructor args. +const getESLintConstructorArgs = async cliOptions => { + // these are not constructor args for either the legacy or the flat ESLint + // api + const { fixDryRun, format, maxWarnings, quiet, ...legacyConstructorArgs } = + cliOptions; + + if (await shouldUseFlatConfig()) { + // these options are supported by the legacy ESLint api but aren't + // supported by the ESLintFlat api + const { + extensions, + ignorePath, + rulePaths, + resolvePluginsRelativeTo, + useEslintrc, + overrideConfig, + ...flatConstructorArgs + } = legacyConstructorArgs; + return flatConstructorArgs; + } + + return legacyConstructorArgs; +}; + let cachedValues; -const getCachedValues = (config, extraOptions) => { +const getCachedValues = async (config, extraOptions) => { if (!cachedValues) { const { cliOptions: baseCliOptions } = getESLintOptions(config); const cliOptions = { @@ -106,20 +165,20 @@ const getCachedValues = (config, extraOptions) => { ...extraOptions, }; - // these are not constructor args, so remove them - const { fixDryRun, format, maxWarnings, quiet, ...eslintOptions } = - cliOptions; - - const cli = new ESLint(eslintOptions); + const ESLintConstructor = await getESLintConstructor(); + const cli = new ESLintConstructor( + await getESLintConstructorArgs(cliOptions), + ); cachedValues = { isPathIgnored: cli.isPathIgnored.bind(cli), lintFiles: (...args) => cli.lintFiles(...args), formatter: async (...args) => { - const formatter = await cli.loadFormatter(format); + const formatter = await cli.loadFormatter(cliOptions.format); return formatter.format(...args); }, cliOptions, + ESLintConstructor, }; } @@ -139,10 +198,8 @@ const runESLint = async ({ testPath, config, extraOptions }) => { config.setupFilesAfterEnv.forEach(require); } - const { isPathIgnored, lintFiles, formatter, cliOptions } = getCachedValues( - config, - extraOptions, - ); + const { isPathIgnored, lintFiles, formatter, cliOptions, ESLintConstructor } = + await getCachedValues(config, extraOptions); if (await isPathIgnored(testPath)) { return mkTestResults({ @@ -163,13 +220,13 @@ const runESLint = async ({ testPath, config, extraOptions }) => { const report = await lintFiles([testPath]); if (cliOptions.fix && !cliOptions.fixDryRun) { - await ESLint.outputFixes(report); + await ESLintConstructor.outputFixes(report); } const end = Date.now(); const message = await formatter( - cliOptions.quiet ? ESLint.getErrorResults(report) : report, + cliOptions.quiet ? ESLintConstructor.getErrorResults(report) : report, ); if (report[0]?.errorCount > 0) {