diff --git a/packages/angular/src/generators/add-linting/add-linting.spec.ts b/packages/angular/src/generators/add-linting/add-linting.spec.ts index b0eadabf176979..896c651b659ce3 100644 --- a/packages/angular/src/generators/add-linting/add-linting.spec.ts +++ b/packages/angular/src/generators/add-linting/add-linting.spec.ts @@ -65,7 +65,7 @@ describe('addLinting generator', () => { }); const { devDependencies } = readJson(tree, 'package.json'); - expect(devDependencies['@typescript-eslint/utils']).toBe('^8.40.0'); + expect(devDependencies['@typescript-eslint/utils']).toBe('^8.57.0'); delete process.env.ESLINT_USE_FLAT_CONFIG; }); diff --git a/packages/angular/src/generators/add-linting/lib/add-angular-eslint-dependencies.ts b/packages/angular/src/generators/add-linting/lib/add-angular-eslint-dependencies.ts index 248fb56ac2df5a..9f3c107b9cbbfd 100644 --- a/packages/angular/src/generators/add-linting/lib/add-angular-eslint-dependencies.ts +++ b/packages/angular/src/generators/add-linting/lib/add-angular-eslint-dependencies.ts @@ -4,7 +4,7 @@ import { type Tree, } from '@nx/devkit'; import { useFlatConfig } from '@nx/eslint/src/utils/flat-config'; -import { eslint9__typescriptESLintVersion } from '@nx/eslint/src/utils/versions'; +import { versions as eslintPkgVersions } from '@nx/eslint/src/utils/version-utils'; import { versions } from '../../utils/version-utils'; import { isBuildableLibraryProject } from './buildable-project'; @@ -27,15 +27,20 @@ export function addAngularEsLintDependencies( if ('typescriptEslintVersion' in compatVersions) { devDependencies['@typescript-eslint/utils'] = usesEslintFlatConfig - ? eslint9__typescriptESLintVersion + ? eslintPkgVersions(tree).typescriptESLintVersion : compatVersions.typescriptEslintVersion; } if (isBuildableLibraryProject(tree, projectName)) { - const jsoncEslintParserVersionToInstall = - versions(tree).jsoncEslintParserVersion; - devDependencies['jsonc-eslint-parser'] = jsoncEslintParserVersionToInstall; + devDependencies['jsonc-eslint-parser'] = + compatVersions.jsoncEslintParserVersion; } - return addDependenciesToPackageJson(tree, {}, devDependencies); + return addDependenciesToPackageJson( + tree, + {}, + devDependencies, + undefined, + true + ); } diff --git a/packages/eslint/src/generators/convert-to-flat-config/generator.spec.ts b/packages/eslint/src/generators/convert-to-flat-config/generator.spec.ts index 901577338821f9..e498fff79d381f 100644 --- a/packages/eslint/src/generators/convert-to-flat-config/generator.spec.ts +++ b/packages/eslint/src/generators/convert-to-flat-config/generator.spec.ts @@ -67,11 +67,11 @@ describe('convert-to-flat-config generator', () => { "devDependencies": { "@nx/eslint": "0.0.1", "@nx/eslint-plugin": "0.0.1", - "@typescript-eslint/eslint-plugin": "^8.40.0", - "@typescript-eslint/parser": "^8.40.0", - "eslint": "^9.8.0", + "@typescript-eslint/eslint-plugin": "^8.57.0", + "@typescript-eslint/parser": "^8.57.0", + "eslint": "^10.0.0", "eslint-config-prettier": "^10.0.0", - "typescript-eslint": "^8.40.0" + "typescript-eslint": "^8.57.0" } } " @@ -667,11 +667,11 @@ describe('convert-to-flat-config generator', () => { "devDependencies": { "@nx/eslint": "0.0.1", "@nx/eslint-plugin": "0.0.1", - "@typescript-eslint/eslint-plugin": "^8.40.0", - "@typescript-eslint/parser": "^8.40.0", - "eslint": "^9.8.0", + "@typescript-eslint/eslint-plugin": "^8.57.0", + "@typescript-eslint/parser": "^8.57.0", + "eslint": "^10.0.0", "eslint-config-prettier": "^10.0.0", - "typescript-eslint": "^8.40.0" + "typescript-eslint": "^8.57.0" } } " diff --git a/packages/eslint/src/generators/convert-to-flat-config/generator.ts b/packages/eslint/src/generators/convert-to-flat-config/generator.ts index 8d0805cd771b6d..bfd35f136de4ab 100644 --- a/packages/eslint/src/generators/convert-to-flat-config/generator.ts +++ b/packages/eslint/src/generators/convert-to-flat-config/generator.ts @@ -18,13 +18,7 @@ import { ConvertToFlatConfigGeneratorSchema } from './schema'; import { findEslintFile } from '../utils/eslint-file'; import { hasEslintPlugin } from '../utils/plugin'; import { join } from 'path'; -import { - eslint9__eslintVersion, - eslint9__typescriptESLintVersion, - eslintConfigPrettierVersion, - eslintrcVersion, - eslintVersion, -} from '../../utils/versions'; +import { versions } from '../../utils/version-utils'; import { ESLint } from 'eslint'; import { convertEslintJsonToFlatConfig } from './converters/json-converter'; @@ -262,33 +256,34 @@ function processConvertedConfig( tree.write(join(root, target), content); // These dependencies are required for flat configs that are generated by subsequent app/lib generators. + const pkgVersions = versions(tree); const devDependencies: Record = { - eslint: eslint9__eslintVersion, - 'eslint-config-prettier': eslintConfigPrettierVersion, - 'typescript-eslint': eslint9__typescriptESLintVersion, - '@typescript-eslint/eslint-plugin': eslint9__typescriptESLintVersion, - '@typescript-eslint/parser': eslint9__typescriptESLintVersion, + eslint: pkgVersions.eslintVersion, + 'eslint-config-prettier': pkgVersions.eslintConfigPrettierVersion, + 'typescript-eslint': pkgVersions.typescriptESLintVersion, + '@typescript-eslint/eslint-plugin': pkgVersions.typescriptESLintVersion, + '@typescript-eslint/parser': pkgVersions.typescriptESLintVersion, }; if (getDependencyVersionFromPackageJson(tree, '@typescript-eslint/utils')) { devDependencies['@typescript-eslint/utils'] = - eslint9__typescriptESLintVersion; + pkgVersions.typescriptESLintVersion; } if ( getDependencyVersionFromPackageJson(tree, '@typescript-eslint/type-utils') ) { devDependencies['@typescript-eslint/type-utils'] = - eslint9__typescriptESLintVersion; + pkgVersions.typescriptESLintVersion; } // add missing packages if (addESLintRC) { - devDependencies['@eslint/eslintrc'] = eslintrcVersion; + devDependencies['@eslint/eslintrc'] = pkgVersions.eslintrcVersion; } if (addESLintJS) { - devDependencies['@eslint/js'] = eslintVersion; + devDependencies['@eslint/js'] = pkgVersions.eslintJsVersion; } - addDependenciesToPackageJson(tree, {}, devDependencies); + addDependenciesToPackageJson(tree, {}, devDependencies, undefined, true); } diff --git a/packages/eslint/src/generators/init/init-migration.ts b/packages/eslint/src/generators/init/init-migration.ts index cb83c1ff93475c..b85aeb0d9a80f6 100644 --- a/packages/eslint/src/generators/init/init-migration.ts +++ b/packages/eslint/src/generators/init/init-migration.ts @@ -20,7 +20,8 @@ import { getGlobalFlatEslintConfiguration, } from './global-eslint-config'; import { useFlatConfig } from '../../utils/flat-config'; -import { eslintVersion, nxVersion } from '../../utils/versions'; +import { versions } from '../../utils/version-utils'; +import { nxVersion } from '../../utils/versions'; import { addBlockToFlatConfigExport, addImportToFlatConfig, @@ -69,10 +70,10 @@ export function migrateConfigToMonorepoStyle( tree, {}, { - '@eslint/js': eslintVersion, + '@eslint/js': versions(tree).eslintJsVersion, }, undefined, - keepExistingVersions + keepExistingVersions ?? true ); tree.write( tree.exists(`eslint.config.${eslintConfigFormat}`) @@ -131,7 +132,9 @@ export function migrateConfigToMonorepoStyle( {}, { '@nx/eslint-plugin': nxVersion, - } + }, + undefined, + true ); } diff --git a/packages/eslint/src/generators/init/init.ts b/packages/eslint/src/generators/init/init.ts index b75b98b3139852..5b9351004f2be7 100644 --- a/packages/eslint/src/generators/init/init.ts +++ b/packages/eslint/src/generators/init/init.ts @@ -10,7 +10,8 @@ import { updateNxJson, } from '@nx/devkit'; import { addPlugin } from '@nx/devkit/src/utils/add-plugin'; -import { eslintVersion, nxVersion } from '../../utils/versions'; +import { nxVersion } from '../../utils/versions'; +import { versions } from '../../utils/version-utils'; import { determineEslintConfigFormat, findEslintFile, @@ -155,10 +156,10 @@ export async function initEsLint( {}, { '@nx/eslint': nxVersion, - eslint: eslintVersion, + eslint: versions(tree).eslintVersion, }, undefined, - options.keepExistingVersions + options.keepExistingVersions ?? true ) ); } diff --git a/packages/eslint/src/generators/lint-project/lint-project.ts b/packages/eslint/src/generators/lint-project/lint-project.ts index 8ba465bb86ab53..b84110ddc841f4 100644 --- a/packages/eslint/src/generators/lint-project/lint-project.ts +++ b/packages/eslint/src/generators/lint-project/lint-project.ts @@ -39,7 +39,7 @@ import { BASE_ESLINT_CONFIG_FILENAMES, } from '../../utils/config-file'; import { hasEslintPlugin } from '../utils/plugin'; -import { jsoncEslintParserVersion } from '../../utils/versions'; +import { versions } from '../../utils/version-utils'; import { setupRootEsLint } from './setup-root-eslint'; import { getProjectType } from '@nx/js/src/utils/typescript/ts-solution-setup'; @@ -184,7 +184,7 @@ export async function lintProjectGeneratorInternal( addDependenciesToPackageJson( tree, {}, - { 'jsonc-eslint-parser': jsoncEslintParserVersion }, + { 'jsonc-eslint-parser': versions(tree).jsoncEslintParserVersion }, undefined, true ) diff --git a/packages/eslint/src/generators/lint-project/setup-root-eslint.ts b/packages/eslint/src/generators/lint-project/setup-root-eslint.ts index ae93b0ae00fc4d..3ad9e230387f4d 100644 --- a/packages/eslint/src/generators/lint-project/setup-root-eslint.ts +++ b/packages/eslint/src/generators/lint-project/setup-root-eslint.ts @@ -5,13 +5,8 @@ import { type Tree, } from '@nx/devkit'; import { useFlatConfig } from '../../utils/flat-config'; -import { - eslint9__eslintVersion, - eslint9__typescriptESLintVersion, - eslintConfigPrettierVersion, - nxVersion, - typescriptESLintVersion, -} from '../../utils/versions'; +import { versions } from '../../utils/version-utils'; +import { nxVersion } from '../../utils/versions'; import { getGlobalEsLintConfiguration, getGlobalFlatEslintConfiguration, @@ -58,18 +53,22 @@ function setUpLegacyRootEslintRc(tree: Tree, options: SetupRootEsLintOptions) { tree.write('.eslintignore', 'node_modules\n'); } - return !options.skipPackageJson - ? addDependenciesToPackageJson( - tree, - {}, - { - '@nx/eslint-plugin': nxVersion, - '@typescript-eslint/parser': typescriptESLintVersion, - '@typescript-eslint/eslint-plugin': typescriptESLintVersion, - 'eslint-config-prettier': eslintConfigPrettierVersion, - } - ) - : () => {}; + if (options.skipPackageJson) { + return () => {}; + } + const pkgVersions = versions(tree); + return addDependenciesToPackageJson( + tree, + {}, + { + '@nx/eslint-plugin': nxVersion, + '@typescript-eslint/parser': pkgVersions.typescriptESLintVersion, + '@typescript-eslint/eslint-plugin': pkgVersions.typescriptESLintVersion, + 'eslint-config-prettier': pkgVersions.eslintConfigPrettierVersion, + }, + undefined, + true + ); } function setUpRootFlatConfig(tree: Tree, options: SetupRootEsLintOptions) { @@ -81,17 +80,21 @@ function setUpRootFlatConfig(tree: Tree, options: SetupRootEsLintOptions) { ) ); - return !options.skipPackageJson - ? addDependenciesToPackageJson( - tree, - {}, - { - '@eslint/js': eslint9__eslintVersion, - '@nx/eslint-plugin': nxVersion, - eslint: eslint9__eslintVersion, - 'eslint-config-prettier': eslintConfigPrettierVersion, - 'typescript-eslint': eslint9__typescriptESLintVersion, - } - ) - : () => {}; + if (options.skipPackageJson) { + return () => {}; + } + const pkgVersions = versions(tree); + return addDependenciesToPackageJson( + tree, + {}, + { + '@eslint/js': pkgVersions.eslintJsVersion, + '@nx/eslint-plugin': nxVersion, + eslint: pkgVersions.eslintVersion, + 'eslint-config-prettier': pkgVersions.eslintConfigPrettierVersion, + 'typescript-eslint': pkgVersions.typescriptESLintVersion, + }, + undefined, + true + ); } diff --git a/packages/eslint/src/generators/utils/eslint-file.spec.ts b/packages/eslint/src/generators/utils/eslint-file.spec.ts index 4e147af1fa0f19..f1834120dd5455 100644 --- a/packages/eslint/src/generators/utils/eslint-file.spec.ts +++ b/packages/eslint/src/generators/utils/eslint-file.spec.ts @@ -1,6 +1,5 @@ -import { readJson, type Tree } from '@nx/devkit'; +import { readJson, type Tree, updateJson } from '@nx/devkit'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; -import * as devkitInternals from 'nx/src/devkit-internals'; import { BASE_ESLINT_CONFIG_FILENAMES, ESLINT_CONFIG_FILENAMES, @@ -122,10 +121,11 @@ describe('@nx/eslint:lint-file', () => { }); it('should install necessary dependencies', () => { - // mock eslint version - jest.spyOn(devkitInternals, 'readModulePackageJson').mockReturnValue({ - packageJson: { name: 'eslint', version: '9.0.0' }, - path: '', + // Declare eslint v9 in the workspace. + updateJson(tree, 'package.json', (json) => { + json.devDependencies ??= {}; + json.devDependencies.eslint = '^9.0.0'; + return json; }); tree.write('eslint.config.cjs', 'module.exports = {};'); tree.write( @@ -154,8 +154,9 @@ module.exports = [ expect(readJson(tree, 'package.json').devDependencies) .toMatchInlineSnapshot(` { - "@eslint/compat": "^1.1.1", - "@eslint/eslintrc": "^2.1.1", + "@eslint/compat": "^1.4.1", + "@eslint/eslintrc": "^3.3.0", + "eslint": "^9.0.0", } `); }); @@ -211,10 +212,11 @@ module.exports = [ }); it('should add wrapped plugin for compat in extends when using eslint v9', () => { - // mock eslint version - jest.spyOn(devkitInternals, 'readModulePackageJson').mockReturnValue({ - packageJson: { name: 'eslint', version: '9.0.0' }, - path: '', + // Declare eslint v9 in the workspace. + updateJson(tree, 'package.json', (json) => { + json.devDependencies ??= {}; + json.devDependencies.eslint = '^9.0.0'; + return json; }); tree.write('eslint.config.cjs', 'module.exports = {};'); tree.write( @@ -270,10 +272,11 @@ module.exports = [ }); it('should handle mixed multiple incompatible and compatible plugins and add them to extends in the specified order when using eslint v9', () => { - // mock eslint version - jest.spyOn(devkitInternals, 'readModulePackageJson').mockReturnValue({ - packageJson: { name: 'eslint', version: '9.0.0' }, - path: '', + // Declare eslint v9 in the workspace. + updateJson(tree, 'package.json', (json) => { + json.devDependencies ??= {}; + json.devDependencies.eslint = '^9.0.0'; + return json; }); tree.write('eslint.config.cjs', 'module.exports = {};'); tree.write( @@ -341,10 +344,12 @@ module.exports = [ }); it('should not add wrapped plugin for compat in extends when not using eslint v9', () => { - // mock eslint version - jest.spyOn(devkitInternals, 'readModulePackageJson').mockReturnValue({ - packageJson: { name: 'eslint', version: '8.0.0' }, - path: '', + // Declare eslint v8 in the workspace so the helper picks the + // pre-flat-config branch. + updateJson(tree, 'package.json', (json) => { + json.devDependencies ??= {}; + json.devDependencies.eslint = '~8.0.0'; + return json; }); tree.write('eslint.config.cjs', 'module.exports = {};'); tree.write( diff --git a/packages/eslint/src/generators/utils/eslint-file.ts b/packages/eslint/src/generators/utils/eslint-file.ts index 4fc47d169f2951..ce232ad15727d7 100644 --- a/packages/eslint/src/generators/utils/eslint-file.ts +++ b/packages/eslint/src/generators/utils/eslint-file.ts @@ -9,7 +9,7 @@ import { updateJson, } from '@nx/devkit'; import type { Linter } from 'eslint'; -import { gte } from 'semver'; +import { coerce } from 'semver'; import { baseEsLintConfigFile, ESLINT_CONFIG_FILENAMES, @@ -20,12 +20,7 @@ import { eslintFlatConfigFilenames, useFlatConfig, } from '../../utils/flat-config'; -import { getInstalledEslintVersion } from '../../utils/version-utils'; -import { - eslint9__eslintVersion, - eslintCompat, - eslintrcVersion, -} from '../../utils/versions'; +import { versions } from '../../utils/version-utils'; import { addBlockToFlatConfigExport, addFlatCompatToFlatConfig, @@ -438,10 +433,12 @@ export function addExtendsToLintConfig( : 'mjs'; let shouldImportEslintCompat = false; - // assume eslint version is 9 if not found, as it's what we'd be generating by default - const eslintVersion = - getInstalledEslintVersion(tree) ?? eslint9__eslintVersion; - if (gte(eslintVersion, '9.0.0')) { + // Resolve the pins for this workspace's installed ESLint major (falling + // back to the latest default when nothing is installed yet, since that's + // what we'd be generating for a new workspace). + const pkgVersions = versions(tree); + const eslintMajor = coerce(pkgVersions.eslintVersion)?.major ?? 0; + if (eslintMajor >= 9) { // eslint v9 requires the incompatible plugins to be wrapped with a helper from @eslint/compat const plugins = (Array.isArray(plugin) ? plugin : [plugin]).map((p) => typeof p === 'string' ? { name: p, needCompatFixup: false } : p @@ -500,7 +497,10 @@ export function addExtendsToLintConfig( return addDependenciesToPackageJson( tree, {}, - { '@eslint/compat': eslintCompat, '@eslint/eslintrc': eslintrcVersion }, + { + '@eslint/compat': pkgVersions.eslintCompatVersion, + '@eslint/eslintrc': pkgVersions.eslintrcVersion, + }, undefined, true ); @@ -509,7 +509,7 @@ export function addExtendsToLintConfig( return addDependenciesToPackageJson( tree, {}, - { '@eslint/eslintrc': eslintrcVersion }, + { '@eslint/eslintrc': pkgVersions.eslintrcVersion }, undefined, true ); diff --git a/packages/eslint/src/generators/workspace-rule/workspace-rule.ts b/packages/eslint/src/generators/workspace-rule/workspace-rule.ts index 18c0fa42bd3b6d..3a0077ab28a81c 100644 --- a/packages/eslint/src/generators/workspace-rule/workspace-rule.ts +++ b/packages/eslint/src/generators/workspace-rule/workspace-rule.ts @@ -17,7 +17,7 @@ import * as ts from 'typescript'; import { workspaceLintPluginDir } from '../../utils/workspace-lint-rules'; import { lintWorkspaceRulesProjectGenerator } from '../workspace-rules-project/workspace-rules-project'; import { useFlatConfig } from '../../utils/flat-config'; -import { eslint9__typescriptESLintVersion } from '../../utils/versions'; +import { versions } from '../../utils/version-utils'; export interface LintWorkspaceRuleGeneratorOptions { name: string; @@ -48,7 +48,12 @@ export async function lintWorkspaceRuleGenerator( addDependenciesToPackageJson( tree, {}, - { '@typescript-eslint/rule-tester': eslint9__typescriptESLintVersion } + { + '@typescript-eslint/rule-tester': + versions(tree).typescriptESLintVersion, + }, + undefined, + true ) ); } diff --git a/packages/eslint/src/generators/workspace-rules-project/workspace-rules-project.ts b/packages/eslint/src/generators/workspace-rules-project/workspace-rules-project.ts index 0a6d39d01afff1..604477071f43d6 100644 --- a/packages/eslint/src/generators/workspace-rules-project/workspace-rules-project.ts +++ b/packages/eslint/src/generators/workspace-rules-project/workspace-rules-project.ts @@ -18,7 +18,7 @@ import { addSwcRegisterDependencies } from '@nx/js/src/utils/swc/add-swc-depende import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { join } from 'path'; import { nxVersion } from '../../utils/versions'; -import { getTypeScriptEslintVersionToInstall } from '../../utils/version-utils'; +import { versions } from '../../utils/version-utils'; import { workspaceLintPluginDir } from '../../utils/workspace-lint-rules'; export const WORKSPACE_RULES_PROJECT_NAME = 'eslint-rules'; @@ -118,14 +118,15 @@ export async function lintWorkspaceRulesProjectGenerator( // Add swc dependencies tasks.push(addSwcRegisterDependencies(tree)); - const typescriptEslintVersion = getTypeScriptEslintVersionToInstall(tree); tasks.push( addDependenciesToPackageJson( tree, {}, { - '@typescript-eslint/utils': typescriptEslintVersion, - } + '@typescript-eslint/utils': versions(tree).typescriptESLintVersion, + }, + undefined, + true ) ); diff --git a/packages/eslint/src/utils/backward-compatible-versions.ts b/packages/eslint/src/utils/backward-compatible-versions.ts new file mode 100644 index 00000000000000..1383b2977a9cd0 --- /dev/null +++ b/packages/eslint/src/utils/backward-compatible-versions.ts @@ -0,0 +1,38 @@ +import * as latestVersions from './versions'; + +export type PackageVersionNames = Exclude< + keyof typeof latestVersions, + 'nxVersion' +>; + +export type PackageCompatVersions = Record; + +export type VersionMap = { + 10: PackageCompatVersions; + 9: PackageCompatVersions; + 8: PackageCompatVersions; +}; + +export const backwardCompatibleVersions: VersionMap = { + 10: { ...latestVersions }, + 9: { + eslintVersion: '^9.8.0', + eslintJsVersion: '^9.8.0', + eslintrcVersion: '^3.3.0', + eslintConfigPrettierVersion: '^10.0.0', + eslintCompatVersion: '^1.4.1', + typescriptESLintVersion: '^8.40.0', + jsoncEslintParserVersion: '^2.1.0', + }, + 8: { + eslintVersion: '~8.57.0', + // @eslint/js and @eslint/compat are not installed on the legacy .eslintrc + // path, but the compat map must be complete for every major. + eslintJsVersion: '^9.8.0', + eslintrcVersion: '^2.1.1', + eslintConfigPrettierVersion: '^10.0.0', + eslintCompatVersion: '^1.4.1', + typescriptESLintVersion: '^7.16.0', + jsoncEslintParserVersion: '^2.1.0', + }, +}; diff --git a/packages/eslint/src/utils/version-utils.ts b/packages/eslint/src/utils/version-utils.ts index 5077746d65e3a4..5bd145bffe819a 100644 --- a/packages/eslint/src/utils/version-utils.ts +++ b/packages/eslint/src/utils/version-utils.ts @@ -1,47 +1,48 @@ import { getDependencyVersionFromPackageJson, type Tree } from '@nx/devkit'; import { checkAndCleanWithSemver } from '@nx/devkit/src/utils/semver'; import { readModulePackageJson } from 'nx/src/devkit-internals'; -import { lt } from 'semver'; +import { coerce } from 'semver'; import { - eslint9__typescriptESLintVersion, - typescriptESLintVersion, -} from './versions'; + backwardCompatibleVersions, + type PackageCompatVersions, + type PackageVersionNames, +} from './backward-compatible-versions'; +import * as latestVersions from './versions'; export function getInstalledPackageVersion( pkgName: string, tree?: Tree ): string | null { + // When a tree is provided, look up the dependency from the tree's + // package.json so that generators react to the version the user has + // declared for the workspace they are running against, rather than the + // version in the CLI's own node_modules. This matches the `@nx/angular` + // detection pattern and makes the default (when nothing is installed) + // fall back to `latestVersions` rather than to whatever happens to be + // resolved on disk. + if (tree) { + const declared = getDependencyVersionFromPackageJson(tree, pkgName); + if (!declared) { + return null; + } + try { + return checkAndCleanWithSemver(tree, pkgName, declared); + } catch {} + return null; + } + try { const packageJson = readModulePackageJson(pkgName).packageJson; return packageJson.version; } catch {} - // the package is not installed on disk, it could be in the package.json - // but waiting to be installed - let pkgVersionInRootPackageJson: string | null; - if (tree) { - pkgVersionInRootPackageJson = getDependencyVersionFromPackageJson( - tree, - pkgName - ); - } else { - // Use filesystem-based signature for pnpm catalog compatibility - pkgVersionInRootPackageJson = getDependencyVersionFromPackageJson(pkgName); - } - - if (!pkgVersionInRootPackageJson) { - // the package is not installed + const declaredFromFs = getDependencyVersionFromPackageJson(pkgName); + if (!declaredFromFs) { return null; } - try { - // try to parse and return the version - return tree - ? checkAndCleanWithSemver(tree, pkgName, pkgVersionInRootPackageJson) - : checkAndCleanWithSemver(pkgName, pkgVersionInRootPackageJson); + return checkAndCleanWithSemver(pkgName, declaredFromFs); } catch {} - - // we could not resolve the version return null; } @@ -49,10 +50,73 @@ export function getInstalledEslintVersion(tree?: Tree): string | null { return getInstalledPackageVersion('eslint', tree); } -export function getTypeScriptEslintVersionToInstall(tree: Tree): string | null { - const eslintVersion = getInstalledEslintVersion(tree); +export function getInstalledEslintVersionInfo( + tree?: Tree +): { version: string; major: number } | null { + const installed = getInstalledEslintVersion(tree); + if (!installed) { + return null; + } + const coerced = coerce(installed); + if (!coerced) { + return null; + } + return { version: coerced.version, major: coerced.major }; +} + +export function getInstalledEslintMajorVersion(tree?: Tree): number | null { + return getInstalledEslintVersionInfo(tree)?.major ?? null; +} + +/** + * Returns a single version pin for the given package, compatible with the + * given ESLint major version. Falls back to the latest pin when the major is + * unknown (e.g., a future ESLint major we haven't explicitly added support + * for yet). + */ +export function getPkgVersionForEslintMajor( + pkgVersionName: PackageVersionNames, + eslintMajorVersion: number +): string { + return ( + backwardCompatibleVersions[eslintMajorVersion]?.[pkgVersionName] ?? + latestVersions[pkgVersionName] + ); +} + +/** + * Returns the full set of package version pins compatible with the ESLint + * major version installed in the tree. + * + * When ESLint is already installed, the pins are keyed on its major. An + * installed major that is newer than anything we explicitly support falls + * back to `latestVersions`, so new ESLint majors must be added to + * `backwardCompatibleVersions` when they become the default. + * + * When no ESLint is installed, the default is `latestVersions` — unless + * the user has explicitly opted into legacy eslintrc via + * `ESLINT_USE_FLAT_CONFIG=false`, in which case we return the v8 compat + * map (the last major that supported eslintrc natively). Without this + * check we'd install an ESLint major that can't read the `.eslintrc.json` + * we just scaffolded. + * + * We only check the env var (not `useFlatConfig(tree)`) because the tree- + * less fallback in `useFlatConfig` reads the CLI's own `require('eslint')` + * version, which is unrelated to what the generator is about to install. + */ +export function versions(tree: Tree): PackageCompatVersions { + const majorEslintVersion = getInstalledEslintMajorVersion(tree); + const legacyRequested = process.env.ESLINT_USE_FLAT_CONFIG === 'false'; + + if (majorEslintVersion != null) { + if (legacyRequested && majorEslintVersion >= 10) { + throw new Error( + `ESLint v${majorEslintVersion} does not support the legacy "eslintrc" configuration format, but ESLINT_USE_FLAT_CONFIG=false was set. ` + + `Unset the environment variable to scaffold a flat config, or downgrade ESLint to v9 or lower.` + ); + } + return backwardCompatibleVersions[majorEslintVersion] ?? latestVersions; + } - return eslintVersion && lt(eslintVersion, '9.0.0') - ? typescriptESLintVersion - : eslint9__typescriptESLintVersion; + return legacyRequested ? backwardCompatibleVersions[8] : latestVersions; } diff --git a/packages/eslint/src/utils/versions.ts b/packages/eslint/src/utils/versions.ts index 65188492647b78..d258a797297f5d 100644 --- a/packages/eslint/src/utils/versions.ts +++ b/packages/eslint/src/utils/versions.ts @@ -1,12 +1,14 @@ export const nxVersion = require('../../package.json').version; -export const eslintVersion = '~8.57.0'; -export const eslintrcVersion = '^2.1.1'; +// ESLint core +export const eslintVersion = '^10.0.0'; +export const eslintJsVersion = '^10.0.0'; +export const eslintrcVersion = '^3.3.0'; export const eslintConfigPrettierVersion = '^10.0.0'; -export const typescriptESLintVersion = '^7.16.0'; -export const jsoncEslintParserVersion = '^2.1.0'; +export const eslintCompatVersion = '^2.0.5'; + +// typescript-eslint +export const typescriptESLintVersion = '^8.57.0'; -// Updated linting stack for ESLint v9, typescript-eslint v8 -export const eslint9__typescriptESLintVersion = '^8.40.0'; -export const eslint9__eslintVersion = '^9.8.0'; -export const eslintCompat = '^1.1.1'; +// parsers +export const jsoncEslintParserVersion = '^2.1.0'; diff --git a/packages/vue/package.json b/packages/vue/package.json index d1688ee5152961..13058b71219207 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -37,7 +37,6 @@ "@nx/vitest": "workspace:*", "@nx/web": "workspace:*", "picomatch": "catalog:", - "semver": "catalog:", "tslib": "catalog:typescript" }, "devDependencies": { diff --git a/packages/vue/src/generators/library/__snapshots__/library.spec.ts.snap b/packages/vue/src/generators/library/__snapshots__/library.spec.ts.snap index f4ca0e3c77eea5..2ec22e6c108768 100644 --- a/packages/vue/src/generators/library/__snapshots__/library.spec.ts.snap +++ b/packages/vue/src/generators/library/__snapshots__/library.spec.ts.snap @@ -50,17 +50,17 @@ exports[`library should add vue and vitest to package.json when non-buildable 1` "@swc/core": "~1.15.5", "@swc/helpers": "~0.5.18", "@types/node": "20.19.9", - "@typescript-eslint/eslint-plugin": "^7.16.0", - "@typescript-eslint/parser": "^7.16.0", + "@typescript-eslint/eslint-plugin": "^8.57.0", + "@typescript-eslint/parser": "^8.57.0", "@vitejs/plugin-vue": "^6.0.1", "@vitest/coverage-v8": "~4.1.0", "@vitest/ui": "~4.1.0", "@vue/eslint-config-prettier": "^10.2.0", - "@vue/eslint-config-typescript": "^11.0.3", + "@vue/eslint-config-typescript": "^14.7.0", "@vue/test-utils": "^2.4.6", - "eslint": "~8.57.0", + "eslint": "^10.0.0", "eslint-config-prettier": "^10.0.0", - "eslint-plugin-vue": "^9.16.1", + "eslint-plugin-vue": "^10.8.0", "jsdom": "^27.1.0", "prettier": "~3.6.2", "typescript": "~5.9.2", diff --git a/packages/vue/src/utils/add-linting.ts b/packages/vue/src/utils/add-linting.ts index cc8d06af28a30d..74de143de44b8a 100644 --- a/packages/vue/src/utils/add-linting.ts +++ b/packages/vue/src/utils/add-linting.ts @@ -14,20 +14,11 @@ import { updateOverrideInLintConfig, } from '@nx/eslint/src/generators/utils/eslint-file'; import { useFlatConfig } from '@nx/eslint/src/utils/flat-config'; -import { - getInstalledEslintVersion, - getTypeScriptEslintVersionToInstall, -} from '@nx/eslint/src/utils/version-utils'; +import { versions as eslintPkgVersions } from '@nx/eslint/src/utils/version-utils'; import type { Linter as EsLintLinter } from 'eslint'; import { Tree } from 'nx/src/generators/tree'; import { joinPathFragments } from 'nx/src/utils/path'; -import { - eslint9__VueEslintConfigTypescriptVersion, - eslintPluginVueVersion, - vueEslintConfigPrettierVersion, - vueEslintConfigTypescriptVersion, -} from './versions'; -import { lt } from 'semver'; +import { versions } from './version-utils'; export async function addLinting( host: Tree, @@ -77,28 +68,28 @@ export async function addLinting( editEslintConfigFiles(host, options.projectRoot); - const eslintVersion = getInstalledEslintVersion(host); - const devDependencies = { - '@vue/eslint-config-prettier': vueEslintConfigPrettierVersion, + const pkgVersions = versions(host); + const devDependencies: Record = { + '@vue/eslint-config-prettier': pkgVersions.vueEslintConfigPrettierVersion, '@vue/eslint-config-typescript': - eslintVersion && lt(eslintVersion, '9.0.0') - ? vueEslintConfigTypescriptVersion - : eslint9__VueEslintConfigTypescriptVersion, - 'eslint-plugin-vue': eslintPluginVueVersion, + pkgVersions.vueEslintConfigTypescriptVersion, + 'eslint-plugin-vue': pkgVersions.eslintPluginVueVersion, }; if ( isEslintConfigSupported(host, options.projectRoot) && useFlatConfig(host) ) { devDependencies['@typescript-eslint/parser'] = - getTypeScriptEslintVersionToInstall(host); + eslintPkgVersions(host).typescriptESLintVersion; } if (!options.skipPackageJson) { const installTask = addDependenciesToPackageJson( host, {}, - devDependencies + devDependencies, + undefined, + true ); tasks.push(installTask); } diff --git a/packages/vue/src/utils/backward-compatible-versions.ts b/packages/vue/src/utils/backward-compatible-versions.ts new file mode 100644 index 00000000000000..a9aee6a436deef --- /dev/null +++ b/packages/vue/src/utils/backward-compatible-versions.ts @@ -0,0 +1,40 @@ +import * as latestVersions from './versions'; + +// Vue lint packages are keyed on the installed ESLint major, since that is +// what drives their peer-dep compatibility (Vue 2 is not supported by the +// Nx Vue plugin). +// +// Only the linting-related keys vary by ESLint major. Listing them +// explicitly avoids pulling Vue-core versions into the compat map. +export type EslintPackageVersionNames = + | 'vueEslintConfigPrettierVersion' + | 'vueEslintConfigTypescriptVersion' + | 'eslintPluginVueVersion'; + +export type PackageCompatVersions = Record; + +export type VersionMap = { + 10: PackageCompatVersions; + 9: PackageCompatVersions; + 8: PackageCompatVersions; +}; + +export const backwardCompatibleVersions: VersionMap = { + 10: { + vueEslintConfigPrettierVersion: + latestVersions.vueEslintConfigPrettierVersion, + vueEslintConfigTypescriptVersion: + latestVersions.vueEslintConfigTypescriptVersion, + eslintPluginVueVersion: latestVersions.eslintPluginVueVersion, + }, + 9: { + vueEslintConfigPrettierVersion: '^10.2.0', + vueEslintConfigTypescriptVersion: '^14.7.0', + eslintPluginVueVersion: '^10.8.0', + }, + 8: { + vueEslintConfigPrettierVersion: '^10.2.0', + vueEslintConfigTypescriptVersion: '^11.0.3', + eslintPluginVueVersion: '^9.16.1', + }, +}; diff --git a/packages/vue/src/utils/version-utils.ts b/packages/vue/src/utils/version-utils.ts new file mode 100644 index 00000000000000..6ccb6dfba4eb56 --- /dev/null +++ b/packages/vue/src/utils/version-utils.ts @@ -0,0 +1,21 @@ +import type { Tree } from '@nx/devkit'; +import { getInstalledEslintMajorVersion } from '@nx/eslint/src/utils/version-utils'; +import { + backwardCompatibleVersions, + type PackageCompatVersions, +} from './backward-compatible-versions'; + +/** + * Returns Vue lint package pins compatible with the ESLint major installed + * in the tree. Falls back to the latest pins (keyed by the latest supported + * ESLint major) when no ESLint is installed or the installed major isn't + * explicitly supported yet. + */ +export function versions(tree: Tree): PackageCompatVersions { + const majorEslintVersion = getInstalledEslintMajorVersion(tree); + return ( + (majorEslintVersion != null && + backwardCompatibleVersions[majorEslintVersion]) || + backwardCompatibleVersions[10] + ); +} diff --git a/packages/vue/src/utils/versions.ts b/packages/vue/src/utils/versions.ts index fa177593bad2a8..9c4805a637723a 100644 --- a/packages/vue/src/utils/versions.ts +++ b/packages/vue/src/utils/versions.ts @@ -11,9 +11,8 @@ export const vitePluginVueVersion = '^6.0.1'; // linting deps export const vueEslintConfigPrettierVersion = '^10.2.0'; -export const vueEslintConfigTypescriptVersion = '^11.0.3'; -export const eslint9__VueEslintConfigTypescriptVersion = '^14.6.0'; -export const eslintPluginVueVersion = '^9.16.1'; +export const vueEslintConfigTypescriptVersion = '^14.7.0'; +export const eslintPluginVueVersion = '^10.8.0'; // tailwindcss export const postcssVersion = '8.4.21'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aef7ee04070fdd..e3b2b3b4ec7baa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4102,9 +4102,6 @@ importers: picomatch: specifier: 'catalog:' version: 4.0.4 - semver: - specifier: 'catalog:' - version: 7.7.4 tslib: specifier: catalog:typescript version: 2.8.1