diff --git a/packages/eslint/package.json b/packages/eslint/package.json index d175bea53e6..9ee478521da 100644 --- a/packages/eslint/package.json +++ b/packages/eslint/package.json @@ -32,7 +32,7 @@ "executors": "./executors.json", "peerDependencies": { "@zkochan/js-yaml": "0.0.7", - "eslint": "^8.0.0 || ^9.0.0" + "eslint": "^8.0.0 || ^9.0.0 || ^10.0.0" }, "dependencies": { "@nx/devkit": "workspace:*", diff --git a/packages/eslint/src/executors/lint/utility/eslint-utils.spec.ts b/packages/eslint/src/executors/lint/utility/eslint-utils.spec.ts index abc48ee24e2..6ad4b5bdbbe 100644 --- a/packages/eslint/src/executors/lint/utility/eslint-utils.spec.ts +++ b/packages/eslint/src/executors/lint/utility/eslint-utils.spec.ts @@ -1,3 +1,7 @@ +jest.mock('eslint', () => ({ + loadESLint: undefined, +})); + jest.mock('eslint/use-at-your-own-risk', () => ({ LegacyESLint: jest.fn(), })); @@ -7,6 +11,9 @@ import { resolveAndInstantiateESLint } from './eslint-utils'; describe('eslint-utils', () => { beforeEach(() => { + const eslintModule = require('eslint'); + eslintModule.loadESLint = undefined; + jest.clearAllMocks(); }); @@ -54,6 +61,33 @@ describe('eslint-utils', () => { }); }); + it('should create the ESLint instance with loadESLint when available', async () => { + const eslintModule = require('eslint'); + const LoadedESLintClass = jest.fn(); + eslintModule.loadESLint = jest.fn().mockResolvedValue(LoadedESLintClass); + + await resolveAndInstantiateESLint('./.eslintrc.json', {} as any); + + expect(eslintModule.loadESLint).toHaveBeenCalledWith({ + useFlatConfig: false, + }); + expect(LoadedESLintClass).toHaveBeenCalledWith({ + overrideConfigFile: './.eslintrc.json', + fix: false, + cache: false, + cacheLocation: undefined, + cacheStrategy: undefined, + errorOnUnmatchedPattern: false, + + ignorePath: undefined, + reportUnusedDisableDirectives: undefined, + resolvePluginsRelatedTo: undefined, + rulePaths: [], + useEslintrc: true, + }); + expect(LegacyESLint).not.toHaveBeenCalled(); + }); + describe('noEslintrc', () => { it('should create the ESLint instance with "useEslintrc" set to false', async () => { await resolveAndInstantiateESLint(undefined, { @@ -207,5 +241,35 @@ describe('eslint-utils', () => { `"For Flat Config, ESLint removed \`ignorePath\` and so it is not supported as an option. See https://eslint.org/docs/latest/use/configure/configuration-files-new"` ); }); + + it('should resolve flat ESLint v9+ using loadESLint when available', async () => { + const eslintModule = require('eslint'); + const LoadedESLintClass: jest.Mock & { version?: string } = jest.fn(); + LoadedESLintClass.version = '9.0.0'; + eslintModule.loadESLint = jest.fn().mockResolvedValue(LoadedESLintClass); + + await resolveAndInstantiateESLint( + 'eslint.config.mjs', + { + quiet: true, + } as any, + true + ); + + expect(eslintModule.loadESLint).toHaveBeenCalledWith({ + useFlatConfig: true, + }); + expect(LoadedESLintClass).toHaveBeenCalledWith({ + overrideConfigFile: 'eslint.config.mjs', + fix: false, + cache: false, + cacheLocation: undefined, + cacheStrategy: undefined, + errorOnUnmatchedPattern: false, + + ruleFilter: expect.any(Function), + }); + expect(LegacyESLint).not.toHaveBeenCalled(); + }); }); }); diff --git a/packages/eslint/src/executors/lint/utility/eslint-utils.ts b/packages/eslint/src/executors/lint/utility/eslint-utils.ts index 054a7b808f8..1d7b1a9b361 100644 --- a/packages/eslint/src/executors/lint/utility/eslint-utils.ts +++ b/packages/eslint/src/executors/lint/utility/eslint-utils.ts @@ -11,7 +11,6 @@ export async function resolveAndInstantiateESLint( ) { if (useFlatConfig && eslintConfigPath && !isFlatConfig(eslintConfigPath)) { throw new Error( - // todo: add support for eslint.config.mjs, 'When using the new Flat Config with ESLint, all configs must be named eslint.config.js or eslint.config.cjs and .eslintrc files may not be used. See https://eslint.org/docs/latest/use/configure/configuration-files' ); } diff --git a/packages/eslint/src/utils/resolve-eslint-class.ts b/packages/eslint/src/utils/resolve-eslint-class.ts index 9307d3892bb..5526aa37f1d 100644 --- a/packages/eslint/src/utils/resolve-eslint-class.ts +++ b/packages/eslint/src/utils/resolve-eslint-class.ts @@ -5,6 +5,22 @@ export async function resolveESLintClass(opts?: { useFlatConfigOverrideVal: boolean; }): Promise { try { + const shouldESLintUseFlatConfig = + typeof opts?.useFlatConfigOverrideVal === 'boolean' + ? opts.useFlatConfigOverrideVal + : useFlatConfig(); + + // In eslint 8.57.0 (the final v8 version), a dedicated API was added for resolving the correct ESLint class. + const eslintModule = (await import('eslint')) as typeof import('eslint') & { + loadESLint?: (opts: { useFlatConfig: boolean }) => Promise; + }; + + if (typeof eslintModule.loadESLint === 'function') { + return await eslintModule.loadESLint({ + useFlatConfig: shouldESLintUseFlatConfig, + }); + } + // Explicitly use the FlatESLint and LegacyESLint classes here because the ESLint class points at a different one based on ESLint v8 vs ESLint v9 // But the decision on which one to use is not just based on the major version of ESLint. // @ts-expect-error The may be wrong based on our installed eslint version @@ -12,11 +28,6 @@ export async function resolveESLintClass(opts?: { 'eslint/use-at-your-own-risk' ); - const shouldESLintUseFlatConfig = - typeof opts?.useFlatConfigOverrideVal === 'boolean' - ? opts.useFlatConfigOverrideVal - : useFlatConfig(); - return shouldESLintUseFlatConfig ? FlatESLint : LegacyESLint; } catch { throw new Error( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a4571f2264f..d95a15c2028 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3116,7 +3116,7 @@ importers: specifier: 0.0.7 version: 0.0.7 eslint: - specifier: ^8.0.0 || ^9.0.0 + specifier: ^8.0.0 || ^9.0.0 || ^10.0.0 version: 8.57.0 semver: specifier: 'catalog:'