diff --git a/package.json b/package.json index 431e0a6145..1176593d1d 100644 --- a/package.json +++ b/package.json @@ -110,7 +110,7 @@ "@types/cytoscape": "^3.18.2", "@types/detect-port": "^1.3.2", "@types/ejs": "3.1.2", - "@types/eslint": "~8.44.2", + "@types/eslint": "~8.56.10", "@types/express": "4.17.14", "@types/flat": "^5.0.1", "@types/fs-extra": "^11.0.0", diff --git a/packages/eslint-plugin/src/rules/dependency-checks.ts b/packages/eslint-plugin/src/rules/dependency-checks.ts index 20541426e8..2cad433f93 100644 --- a/packages/eslint-plugin/src/rules/dependency-checks.ts +++ b/packages/eslint-plugin/src/rules/dependency-checks.ts @@ -1,17 +1,21 @@ -import { join } from 'path'; -import { satisfies } from 'semver'; +import { NX_VERSION, normalizePath, workspaceRoot } from '@nx/devkit'; +import { findNpmDependencies } from '@nx/js/src/utils/find-npm-dependencies'; +import { ESLintUtils } from '@typescript-eslint/utils'; import { AST } from 'jsonc-eslint-parser'; import { type JSONLiteral } from 'jsonc-eslint-parser/lib/parser/ast'; -import { normalizePath, workspaceRoot, NX_VERSION } from '@nx/devkit'; -import { findNpmDependencies } from '@nx/js/src/utils/find-npm-dependencies'; -import { readProjectGraph } from '../utils/project-graph-utils'; -import { findProject, getSourceFilePath } from '../utils/runtime-lint-utils'; +import { join } from 'path'; +import { satisfies } from 'semver'; import { getAllDependencies, getPackageJson, getProductionDependencies, } from '../utils/package-json-utils'; -import { ESLintUtils } from '@typescript-eslint/utils'; +import { readProjectGraph } from '../utils/project-graph-utils'; +import { + findProject, + getParserServices, + getSourceFilePath, +} from '../utils/runtime-lint-utils'; export type Options = [ { @@ -96,10 +100,10 @@ export default ESLintUtils.RuleCreator( }, ] ) { - if (!(context.parserServices as any).isJSON) { + if (!getParserServices(context).isJSON) { return {}; } - const fileName = normalizePath(context.getFilename()); + const fileName = normalizePath(context.filename ?? context.getFilename()); // support only package.json if (!fileName.endsWith('/package.json')) { return {}; diff --git a/packages/eslint-plugin/src/rules/enforce-module-boundaries.ts b/packages/eslint-plugin/src/rules/enforce-module-boundaries.ts index 0051a2abf2..0fc742b5a6 100644 --- a/packages/eslint-plugin/src/rules/enforce-module-boundaries.ts +++ b/packages/eslint-plugin/src/rules/enforce-module-boundaries.ts @@ -215,7 +215,7 @@ export default ESLintUtils.RuleCreator( const projectPath = normalizePath( (global as any).projectPath || workspaceRoot ); - const fileName = normalizePath(context.getFilename()); + const fileName = normalizePath(context.filename ?? context.getFilename()); const { projectGraph, diff --git a/packages/eslint-plugin/src/rules/nx-plugin-checks.ts b/packages/eslint-plugin/src/rules/nx-plugin-checks.ts index 7f236cc27b..7bbf3c7017 100644 --- a/packages/eslint-plugin/src/rules/nx-plugin-checks.ts +++ b/packages/eslint-plugin/src/rules/nx-plugin-checks.ts @@ -1,19 +1,23 @@ -import type { AST } from 'jsonc-eslint-parser'; import type { TSESLint } from '@typescript-eslint/utils'; import { ESLintUtils } from '@typescript-eslint/utils'; +import type { AST } from 'jsonc-eslint-parser'; import { ProjectGraphProjectNode, readJsonFile, workspaceRoot, } from '@nx/devkit'; -import { findProject, getSourceFilePath } from '../utils/runtime-lint-utils'; -import { existsSync } from 'fs'; -import { registerTsProject } from '@nx/js/src/internal'; -import * as path from 'path'; -import { readProjectGraph } from '../utils/project-graph-utils'; -import { valid } from 'semver'; import { getRootTsConfigPath } from '@nx/js'; +import { registerTsProject } from '@nx/js/src/internal'; +import { existsSync } from 'fs'; +import * as path from 'path'; +import { valid } from 'semver'; +import { readProjectGraph } from '../utils/project-graph-utils'; +import { + findProject, + getParserServices, + getSourceFilePath, +} from '../utils/runtime-lint-utils'; type Options = [ { @@ -113,14 +117,14 @@ export default ESLintUtils.RuleCreator(() => ``)({ defaultOptions: [DEFAULT_OPTIONS], create(context) { // jsonc-eslint-parser adds this property to parserServices where appropriate - if (!(context.parserServices as any).isJSON) { + if (!getParserServices(context).isJSON) { return {}; } const { projectGraph, projectRootMappings } = readProjectGraph(RULE_NAME); const sourceFilePath = getSourceFilePath( - context.getFilename(), + context.filename ?? context.getFilename(), workspaceRoot ); @@ -301,7 +305,7 @@ export function validateEntry( }); } else { const schemaFilePath = path.join( - path.dirname(context.getFilename()), + path.dirname(context.filename ?? context.getFilename()), schemaNode.value.value ); if (!existsSync(schemaFilePath)) { @@ -399,7 +403,7 @@ export function validateImplemenationNode( let resolvedPath: string; const modulePath = path.join( - path.dirname(context.getFilename()), + path.dirname(context.filename ?? context.getFilename()), implementationPath ); diff --git a/packages/eslint-plugin/src/utils/runtime-lint-utils.ts b/packages/eslint-plugin/src/utils/runtime-lint-utils.ts index 3ee8f9452f..d13a61fce4 100644 --- a/packages/eslint-plugin/src/utils/runtime-lint-utils.ts +++ b/packages/eslint-plugin/src/utils/runtime-lint-utils.ts @@ -1,5 +1,3 @@ -import * as path from 'path'; -import { join } from 'path'; import { DependencyType, joinPathFragments, @@ -11,18 +9,19 @@ import { ProjectGraphProjectNode, workspaceRoot, } from '@nx/devkit'; -import { getPath, pathExists } from './graph-utils'; -import { readFileIfExisting } from 'nx/src/utils/fileutils'; -import { - findProjectForPath, - ProjectRootMappings, -} from 'nx/src/project-graph/utils/find-project-for-path'; import { getRootTsConfigFileName } from '@nx/js'; import { resolveModuleByImport, TargetProjectLocator, } from '@nx/js/src/internal'; -import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils'; +import { AST_NODE_TYPES, TSESLint, TSESTree } from '@typescript-eslint/utils'; +import * as path from 'node:path'; +import { + findProjectForPath, + ProjectRootMappings, +} from 'nx/src/project-graph/utils/find-project-for-path'; +import { readFileIfExisting } from 'nx/src/utils/fileutils'; +import { getPath, pathExists } from './graph-utils'; export type Deps = { [projectName: string]: ProjectGraphDependency[] }; type SingleSourceTagConstraint = { @@ -393,7 +392,7 @@ function packageExistsInPackageJson( projectRoot: string ): boolean { const content = readFileIfExisting( - join(workspaceRoot, projectRoot, 'package.json') + path.join(workspaceRoot, projectRoot, 'package.json') ); if (content) { const { dependencies, devDependencies, peerDependencies } = @@ -499,7 +498,7 @@ export function belongsToDifferentNgEntryPoint( const resolvedImportFile = resolveModuleByImport( importExpr, filePath, // not strictly necessary, but speeds up resolution - join(workspaceRoot, getRootTsConfigFileName()) + path.join(workspaceRoot, getRootTsConfigFileName()) ); if (!resolvedImportFile) { @@ -560,3 +559,22 @@ export function appIsMFERemote(project: ProjectGraphProjectNode): boolean { return false; } + +/** + * parserServices moved from the context object to the nested sourceCode object in v8, + * and was removed from its original location in v9. + */ +export function getParserServices( + context: Readonly> +): any { + if (context.sourceCode && context.sourceCode.parserServices) { + return context.sourceCode.parserServices; + } + const parserServices = context.parserServices; + if (!parserServices) { + throw new Error( + 'Parser Services are not available, please check your ESLint configuration' + ); + } + return parserServices; +} diff --git a/packages/eslint/package.json b/packages/eslint/package.json index 64e0ee24f2..34b777b938 100644 --- a/packages/eslint/package.json +++ b/packages/eslint/package.json @@ -35,7 +35,7 @@ "dependencies": { "@nx/devkit": "file:../devkit", "@nx/js": "file:../js", - "eslint": "^8.0.0", + "eslint": "^8.0.0 || ^9.0.0", "tslib": "^2.3.0", "typescript": "~5.4.2" }, diff --git a/packages/eslint/src/executors/lint/utility/eslint-utils.ts b/packages/eslint/src/executors/lint/utility/eslint-utils.ts index 723e6799a1..2456997539 100644 --- a/packages/eslint/src/executors/lint/utility/eslint-utils.ts +++ b/packages/eslint/src/executors/lint/utility/eslint-utils.ts @@ -1,21 +1,7 @@ import type { ESLint } from 'eslint'; +import { resolveESLintClass } from '../../../utils/resolve-eslint-class'; import type { Schema } from '../schema'; -async function resolveESLintClass( - useFlatConfig = false -): Promise { - try { - if (!useFlatConfig) { - return (await import('eslint')).ESLint; - } - // eslint-disable-next-line @typescript-eslint/no-var-requires - const { FlatESLint } = require('eslint/use-at-your-own-risk'); - return FlatESLint; - } catch { - throw new Error('Unable to find ESLint. Ensure ESLint is installed.'); - } -} - export async function resolveAndInstantiateESLint( eslintConfigPath: string | undefined, options: Schema, diff --git a/packages/eslint/src/plugins/plugin.spec.ts b/packages/eslint/src/plugins/plugin.spec.ts index db7a928423..17b59a2c74 100644 --- a/packages/eslint/src/plugins/plugin.spec.ts +++ b/packages/eslint/src/plugins/plugin.spec.ts @@ -108,6 +108,9 @@ describe('@nx/eslint/plugin', () => { ], "options": { "cwd": ".", + "env": { + "ESLINT_USE_FLAT_CONFIG": "false", + }, }, "outputs": [ "{options.outputFile}", @@ -180,6 +183,9 @@ describe('@nx/eslint/plugin', () => { ], "options": { "cwd": "apps/my-app", + "env": { + "ESLINT_USE_FLAT_CONFIG": "false", + }, }, "outputs": [ "{options.outputFile}", @@ -221,6 +227,9 @@ describe('@nx/eslint/plugin', () => { ], "options": { "cwd": "apps/my-app", + "env": { + "ESLINT_USE_FLAT_CONFIG": "false", + }, }, "outputs": [ "{options.outputFile}", @@ -334,6 +343,9 @@ describe('@nx/eslint/plugin', () => { ], "options": { "cwd": "apps/my-app", + "env": { + "ESLINT_USE_FLAT_CONFIG": "false", + }, }, "outputs": [ "{options.outputFile}", @@ -359,6 +371,9 @@ describe('@nx/eslint/plugin', () => { ], "options": { "cwd": "libs/my-lib", + "env": { + "ESLINT_USE_FLAT_CONFIG": "false", + }, }, "outputs": [ "{options.outputFile}", @@ -444,6 +459,9 @@ describe('@nx/eslint/plugin', () => { ], "options": { "cwd": "apps/my-app", + "env": { + "ESLINT_USE_FLAT_CONFIG": "false", + }, }, "outputs": [ "{options.outputFile}", @@ -470,6 +488,9 @@ describe('@nx/eslint/plugin', () => { ], "options": { "cwd": "libs/my-lib", + "env": { + "ESLINT_USE_FLAT_CONFIG": "false", + }, }, "outputs": [ "{options.outputFile}", @@ -513,6 +534,9 @@ describe('@nx/eslint/plugin', () => { ], "options": { "cwd": "apps/myapp", + "env": { + "ESLINT_USE_FLAT_CONFIG": "false", + }, }, "outputs": [ "{options.outputFile}", @@ -561,6 +585,9 @@ describe('@nx/eslint/plugin', () => { ], "options": { "cwd": "apps/myapp/nested/mylib", + "env": { + "ESLINT_USE_FLAT_CONFIG": "false", + }, }, "outputs": [ "{options.outputFile}", diff --git a/packages/eslint/src/plugins/plugin.ts b/packages/eslint/src/plugins/plugin.ts index 9213988f17..4b9b9a28e6 100644 --- a/packages/eslint/src/plugins/plugin.ts +++ b/packages/eslint/src/plugins/plugin.ts @@ -4,7 +4,6 @@ import { CreateNodesResult, TargetConfiguration, } from '@nx/devkit'; -import type { ESLint } from 'eslint'; import { existsSync } from 'node:fs'; import { dirname, join, normalize, sep } from 'node:path'; import { combineGlobPatterns } from 'nx/src/utils/globs'; @@ -15,6 +14,7 @@ import { baseEsLintFlatConfigFile, isFlatConfig, } from '../utils/config-file'; +import { resolveESLintClass } from '../utils/resolve-eslint-class'; export interface EslintPluginOptions { targetName?: string; @@ -66,7 +66,7 @@ export const createNodes: CreateNodes = [ ).sort((a, b) => (a !== b && isSubDir(a, b) ? -1 : 1)); const excludePatterns = dedupedProjectRoots.map((root) => `${root}/**/*`); - const ESLint = resolveESLintClass(isFlatConfig(configFilePath)); + const ESLint = await resolveESLintClass(isFlatConfig(configFilePath)); const childProjectRoots = new Set(); await Promise.all( @@ -188,11 +188,12 @@ function buildEslintTargets( ], outputs: ['{options.outputFile}'], }; - if (eslintConfigs.some((config) => isFlatConfig(config))) { - targetConfig.options.env = { - ESLINT_USE_FLAT_CONFIG: 'true', - }; - } + + // Always set the environment variable to ensure that the ESLint CLI can run on eslint v8 and v9 + const useFlatConfig = eslintConfigs.some((config) => isFlatConfig(config)); + targetConfig.options.env = { + ESLINT_USE_FLAT_CONFIG: useFlatConfig ? 'true' : 'false', + }; targets[options.targetName] = targetConfig; @@ -213,18 +214,6 @@ function normalizeOptions(options: EslintPluginOptions): EslintPluginOptions { return options; } -function resolveESLintClass(useFlatConfig = false): typeof ESLint { - try { - if (!useFlatConfig) { - return require('eslint').ESLint; - } - - return require('eslint/use-at-your-own-risk').FlatESLint; - } catch { - throw new Error('Unable to find ESLint. Ensure ESLint is installed.'); - } -} - /** * Determines if `child` is a subdirectory of `parent`. This is a simplified * version that takes into account that paths are always relative to the diff --git a/packages/eslint/src/utils/resolve-eslint-class.ts b/packages/eslint/src/utils/resolve-eslint-class.ts new file mode 100644 index 0000000000..cb6c2190c4 --- /dev/null +++ b/packages/eslint/src/utils/resolve-eslint-class.ts @@ -0,0 +1,22 @@ +import type { ESLint } from 'eslint'; + +export async function resolveESLintClass( + useFlatConfig = false +): Promise { + try { + // In eslint 8.57.0 (the final v8 version), a dedicated API was added for resolving the correct ESLint class. + const eslint = await import('eslint'); + if (typeof (eslint as any).loadESLint === 'function') { + return await (eslint as any).loadESLint({ useFlatConfig }); + } + // If that API is not available (an older version of v8), we need to use the old way of resolving the ESLint class. + if (!useFlatConfig) { + return eslint.ESLint; + } + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { FlatESLint } = require('eslint/use-at-your-own-risk'); + return FlatESLint; + } catch { + throw new Error('Unable to find ESLint. Ensure ESLint is installed.'); + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e68b2ac331..7c97c21185 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -405,8 +405,8 @@ devDependencies: specifier: 3.1.2 version: 3.1.2 '@types/eslint': - specifier: ~8.44.2 - version: 8.44.2 + specifier: ~8.56.10 + version: 8.56.10 '@types/express': specifier: 4.17.14 version: 4.17.14 @@ -12803,13 +12803,13 @@ packages: /@types/eslint-scope@3.7.4: resolution: {integrity: sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==} dependencies: - '@types/eslint': 8.44.2 + '@types/eslint': 8.56.10 '@types/estree': 1.0.5 - /@types/eslint@8.44.2: - resolution: {integrity: sha512-sdPRb9K6iL5XZOmBubg8yiFp5yS/JdUDQsq5e6h95km91MCYMuvp7mh1fjPEYUhvHepKpZOjnEaMBR4PxjWDzg==} + /@types/eslint@8.56.10: + resolution: {integrity: sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==} dependencies: - '@types/estree': 1.0.1 + '@types/estree': 1.0.5 '@types/json-schema': 7.0.12 /@types/estree-jsx@1.0.3: diff --git a/tools/eslint-rules/rules/valid-schema-description.ts b/tools/eslint-rules/rules/valid-schema-description.ts index 74de032e78..174be23b1a 100644 --- a/tools/eslint-rules/rules/valid-schema-description.ts +++ b/tools/eslint-rules/rules/valid-schema-description.ts @@ -24,7 +24,7 @@ export const rule = ESLintUtils.RuleCreator(() => __filename)({ defaultOptions: [], create(context) { // jsonc-eslint-parser adds this property to parserServices where appropriate - if (!(context.parserServices as any).isJSON) { + if (!(context.sourceCode.parserServices as any).isJSON) { return {}; } return {