fix(linter): support eslint v9 (#24632)

This commit is contained in:
James Henry 2024-05-22 19:27:06 +04:00 committed by GitHub
parent 6e6211d072
commit 8cfc0a0c08
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 125 additions and 75 deletions

View File

@ -110,7 +110,7 @@
"@types/cytoscape": "^3.18.2", "@types/cytoscape": "^3.18.2",
"@types/detect-port": "^1.3.2", "@types/detect-port": "^1.3.2",
"@types/ejs": "3.1.2", "@types/ejs": "3.1.2",
"@types/eslint": "~8.44.2", "@types/eslint": "~8.56.10",
"@types/express": "4.17.14", "@types/express": "4.17.14",
"@types/flat": "^5.0.1", "@types/flat": "^5.0.1",
"@types/fs-extra": "^11.0.0", "@types/fs-extra": "^11.0.0",

View File

@ -1,17 +1,21 @@
import { join } from 'path'; import { NX_VERSION, normalizePath, workspaceRoot } from '@nx/devkit';
import { satisfies } from 'semver'; import { findNpmDependencies } from '@nx/js/src/utils/find-npm-dependencies';
import { ESLintUtils } from '@typescript-eslint/utils';
import { AST } from 'jsonc-eslint-parser'; import { AST } from 'jsonc-eslint-parser';
import { type JSONLiteral } from 'jsonc-eslint-parser/lib/parser/ast'; import { type JSONLiteral } from 'jsonc-eslint-parser/lib/parser/ast';
import { normalizePath, workspaceRoot, NX_VERSION } from '@nx/devkit'; import { join } from 'path';
import { findNpmDependencies } from '@nx/js/src/utils/find-npm-dependencies'; import { satisfies } from 'semver';
import { readProjectGraph } from '../utils/project-graph-utils';
import { findProject, getSourceFilePath } from '../utils/runtime-lint-utils';
import { import {
getAllDependencies, getAllDependencies,
getPackageJson, getPackageJson,
getProductionDependencies, getProductionDependencies,
} from '../utils/package-json-utils'; } 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 = [ export type Options = [
{ {
@ -96,10 +100,10 @@ export default ESLintUtils.RuleCreator(
}, },
] ]
) { ) {
if (!(context.parserServices as any).isJSON) { if (!getParserServices(context).isJSON) {
return {}; return {};
} }
const fileName = normalizePath(context.getFilename()); const fileName = normalizePath(context.filename ?? context.getFilename());
// support only package.json // support only package.json
if (!fileName.endsWith('/package.json')) { if (!fileName.endsWith('/package.json')) {
return {}; return {};

View File

@ -215,7 +215,7 @@ export default ESLintUtils.RuleCreator(
const projectPath = normalizePath( const projectPath = normalizePath(
(global as any).projectPath || workspaceRoot (global as any).projectPath || workspaceRoot
); );
const fileName = normalizePath(context.getFilename()); const fileName = normalizePath(context.filename ?? context.getFilename());
const { const {
projectGraph, projectGraph,

View File

@ -1,19 +1,23 @@
import type { AST } from 'jsonc-eslint-parser';
import type { TSESLint } from '@typescript-eslint/utils'; import type { TSESLint } from '@typescript-eslint/utils';
import { ESLintUtils } from '@typescript-eslint/utils'; import { ESLintUtils } from '@typescript-eslint/utils';
import type { AST } from 'jsonc-eslint-parser';
import { import {
ProjectGraphProjectNode, ProjectGraphProjectNode,
readJsonFile, readJsonFile,
workspaceRoot, workspaceRoot,
} from '@nx/devkit'; } 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 { 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 = [ type Options = [
{ {
@ -113,14 +117,14 @@ export default ESLintUtils.RuleCreator(() => ``)<Options, MessageIds>({
defaultOptions: [DEFAULT_OPTIONS], defaultOptions: [DEFAULT_OPTIONS],
create(context) { create(context) {
// jsonc-eslint-parser adds this property to parserServices where appropriate // jsonc-eslint-parser adds this property to parserServices where appropriate
if (!(context.parserServices as any).isJSON) { if (!getParserServices(context).isJSON) {
return {}; return {};
} }
const { projectGraph, projectRootMappings } = readProjectGraph(RULE_NAME); const { projectGraph, projectRootMappings } = readProjectGraph(RULE_NAME);
const sourceFilePath = getSourceFilePath( const sourceFilePath = getSourceFilePath(
context.getFilename(), context.filename ?? context.getFilename(),
workspaceRoot workspaceRoot
); );
@ -301,7 +305,7 @@ export function validateEntry(
}); });
} else { } else {
const schemaFilePath = path.join( const schemaFilePath = path.join(
path.dirname(context.getFilename()), path.dirname(context.filename ?? context.getFilename()),
schemaNode.value.value schemaNode.value.value
); );
if (!existsSync(schemaFilePath)) { if (!existsSync(schemaFilePath)) {
@ -399,7 +403,7 @@ export function validateImplemenationNode(
let resolvedPath: string; let resolvedPath: string;
const modulePath = path.join( const modulePath = path.join(
path.dirname(context.getFilename()), path.dirname(context.filename ?? context.getFilename()),
implementationPath implementationPath
); );

View File

@ -1,5 +1,3 @@
import * as path from 'path';
import { join } from 'path';
import { import {
DependencyType, DependencyType,
joinPathFragments, joinPathFragments,
@ -11,18 +9,19 @@ import {
ProjectGraphProjectNode, ProjectGraphProjectNode,
workspaceRoot, workspaceRoot,
} from '@nx/devkit'; } 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 { getRootTsConfigFileName } from '@nx/js';
import { import {
resolveModuleByImport, resolveModuleByImport,
TargetProjectLocator, TargetProjectLocator,
} from '@nx/js/src/internal'; } 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[] }; export type Deps = { [projectName: string]: ProjectGraphDependency[] };
type SingleSourceTagConstraint = { type SingleSourceTagConstraint = {
@ -393,7 +392,7 @@ function packageExistsInPackageJson(
projectRoot: string projectRoot: string
): boolean { ): boolean {
const content = readFileIfExisting( const content = readFileIfExisting(
join(workspaceRoot, projectRoot, 'package.json') path.join(workspaceRoot, projectRoot, 'package.json')
); );
if (content) { if (content) {
const { dependencies, devDependencies, peerDependencies } = const { dependencies, devDependencies, peerDependencies } =
@ -499,7 +498,7 @@ export function belongsToDifferentNgEntryPoint(
const resolvedImportFile = resolveModuleByImport( const resolvedImportFile = resolveModuleByImport(
importExpr, importExpr,
filePath, // not strictly necessary, but speeds up resolution filePath, // not strictly necessary, but speeds up resolution
join(workspaceRoot, getRootTsConfigFileName()) path.join(workspaceRoot, getRootTsConfigFileName())
); );
if (!resolvedImportFile) { if (!resolvedImportFile) {
@ -560,3 +559,22 @@ export function appIsMFERemote(project: ProjectGraphProjectNode): boolean {
return false; 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<TSESLint.RuleContext<any, any>>
): 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;
}

View File

@ -35,7 +35,7 @@
"dependencies": { "dependencies": {
"@nx/devkit": "file:../devkit", "@nx/devkit": "file:../devkit",
"@nx/js": "file:../js", "@nx/js": "file:../js",
"eslint": "^8.0.0", "eslint": "^8.0.0 || ^9.0.0",
"tslib": "^2.3.0", "tslib": "^2.3.0",
"typescript": "~5.4.2" "typescript": "~5.4.2"
}, },

View File

@ -1,21 +1,7 @@
import type { ESLint } from 'eslint'; import type { ESLint } from 'eslint';
import { resolveESLintClass } from '../../../utils/resolve-eslint-class';
import type { Schema } from '../schema'; import type { Schema } from '../schema';
async function resolveESLintClass(
useFlatConfig = false
): Promise<typeof ESLint> {
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( export async function resolveAndInstantiateESLint(
eslintConfigPath: string | undefined, eslintConfigPath: string | undefined,
options: Schema, options: Schema,

View File

@ -108,6 +108,9 @@ describe('@nx/eslint/plugin', () => {
], ],
"options": { "options": {
"cwd": ".", "cwd": ".",
"env": {
"ESLINT_USE_FLAT_CONFIG": "false",
},
}, },
"outputs": [ "outputs": [
"{options.outputFile}", "{options.outputFile}",
@ -180,6 +183,9 @@ describe('@nx/eslint/plugin', () => {
], ],
"options": { "options": {
"cwd": "apps/my-app", "cwd": "apps/my-app",
"env": {
"ESLINT_USE_FLAT_CONFIG": "false",
},
}, },
"outputs": [ "outputs": [
"{options.outputFile}", "{options.outputFile}",
@ -221,6 +227,9 @@ describe('@nx/eslint/plugin', () => {
], ],
"options": { "options": {
"cwd": "apps/my-app", "cwd": "apps/my-app",
"env": {
"ESLINT_USE_FLAT_CONFIG": "false",
},
}, },
"outputs": [ "outputs": [
"{options.outputFile}", "{options.outputFile}",
@ -334,6 +343,9 @@ describe('@nx/eslint/plugin', () => {
], ],
"options": { "options": {
"cwd": "apps/my-app", "cwd": "apps/my-app",
"env": {
"ESLINT_USE_FLAT_CONFIG": "false",
},
}, },
"outputs": [ "outputs": [
"{options.outputFile}", "{options.outputFile}",
@ -359,6 +371,9 @@ describe('@nx/eslint/plugin', () => {
], ],
"options": { "options": {
"cwd": "libs/my-lib", "cwd": "libs/my-lib",
"env": {
"ESLINT_USE_FLAT_CONFIG": "false",
},
}, },
"outputs": [ "outputs": [
"{options.outputFile}", "{options.outputFile}",
@ -444,6 +459,9 @@ describe('@nx/eslint/plugin', () => {
], ],
"options": { "options": {
"cwd": "apps/my-app", "cwd": "apps/my-app",
"env": {
"ESLINT_USE_FLAT_CONFIG": "false",
},
}, },
"outputs": [ "outputs": [
"{options.outputFile}", "{options.outputFile}",
@ -470,6 +488,9 @@ describe('@nx/eslint/plugin', () => {
], ],
"options": { "options": {
"cwd": "libs/my-lib", "cwd": "libs/my-lib",
"env": {
"ESLINT_USE_FLAT_CONFIG": "false",
},
}, },
"outputs": [ "outputs": [
"{options.outputFile}", "{options.outputFile}",
@ -513,6 +534,9 @@ describe('@nx/eslint/plugin', () => {
], ],
"options": { "options": {
"cwd": "apps/myapp", "cwd": "apps/myapp",
"env": {
"ESLINT_USE_FLAT_CONFIG": "false",
},
}, },
"outputs": [ "outputs": [
"{options.outputFile}", "{options.outputFile}",
@ -561,6 +585,9 @@ describe('@nx/eslint/plugin', () => {
], ],
"options": { "options": {
"cwd": "apps/myapp/nested/mylib", "cwd": "apps/myapp/nested/mylib",
"env": {
"ESLINT_USE_FLAT_CONFIG": "false",
},
}, },
"outputs": [ "outputs": [
"{options.outputFile}", "{options.outputFile}",

View File

@ -4,7 +4,6 @@ import {
CreateNodesResult, CreateNodesResult,
TargetConfiguration, TargetConfiguration,
} from '@nx/devkit'; } from '@nx/devkit';
import type { ESLint } from 'eslint';
import { existsSync } from 'node:fs'; import { existsSync } from 'node:fs';
import { dirname, join, normalize, sep } from 'node:path'; import { dirname, join, normalize, sep } from 'node:path';
import { combineGlobPatterns } from 'nx/src/utils/globs'; import { combineGlobPatterns } from 'nx/src/utils/globs';
@ -15,6 +14,7 @@ import {
baseEsLintFlatConfigFile, baseEsLintFlatConfigFile,
isFlatConfig, isFlatConfig,
} from '../utils/config-file'; } from '../utils/config-file';
import { resolveESLintClass } from '../utils/resolve-eslint-class';
export interface EslintPluginOptions { export interface EslintPluginOptions {
targetName?: string; targetName?: string;
@ -66,7 +66,7 @@ export const createNodes: CreateNodes<EslintPluginOptions> = [
).sort((a, b) => (a !== b && isSubDir(a, b) ? -1 : 1)); ).sort((a, b) => (a !== b && isSubDir(a, b) ? -1 : 1));
const excludePatterns = dedupedProjectRoots.map((root) => `${root}/**/*`); const excludePatterns = dedupedProjectRoots.map((root) => `${root}/**/*`);
const ESLint = resolveESLintClass(isFlatConfig(configFilePath)); const ESLint = await resolveESLintClass(isFlatConfig(configFilePath));
const childProjectRoots = new Set<string>(); const childProjectRoots = new Set<string>();
await Promise.all( await Promise.all(
@ -188,11 +188,12 @@ function buildEslintTargets(
], ],
outputs: ['{options.outputFile}'], outputs: ['{options.outputFile}'],
}; };
if (eslintConfigs.some((config) => isFlatConfig(config))) {
targetConfig.options.env = { // Always set the environment variable to ensure that the ESLint CLI can run on eslint v8 and v9
ESLINT_USE_FLAT_CONFIG: 'true', const useFlatConfig = eslintConfigs.some((config) => isFlatConfig(config));
}; targetConfig.options.env = {
} ESLINT_USE_FLAT_CONFIG: useFlatConfig ? 'true' : 'false',
};
targets[options.targetName] = targetConfig; targets[options.targetName] = targetConfig;
@ -213,18 +214,6 @@ function normalizeOptions(options: EslintPluginOptions): EslintPluginOptions {
return options; 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 * Determines if `child` is a subdirectory of `parent`. This is a simplified
* version that takes into account that paths are always relative to the * version that takes into account that paths are always relative to the

View File

@ -0,0 +1,22 @@
import type { ESLint } from 'eslint';
export async function resolveESLintClass(
useFlatConfig = false
): Promise<typeof ESLint> {
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.');
}
}

12
pnpm-lock.yaml generated
View File

@ -405,8 +405,8 @@ devDependencies:
specifier: 3.1.2 specifier: 3.1.2
version: 3.1.2 version: 3.1.2
'@types/eslint': '@types/eslint':
specifier: ~8.44.2 specifier: ~8.56.10
version: 8.44.2 version: 8.56.10
'@types/express': '@types/express':
specifier: 4.17.14 specifier: 4.17.14
version: 4.17.14 version: 4.17.14
@ -12803,13 +12803,13 @@ packages:
/@types/eslint-scope@3.7.4: /@types/eslint-scope@3.7.4:
resolution: {integrity: sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==} resolution: {integrity: sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==}
dependencies: dependencies:
'@types/eslint': 8.44.2 '@types/eslint': 8.56.10
'@types/estree': 1.0.5 '@types/estree': 1.0.5
/@types/eslint@8.44.2: /@types/eslint@8.56.10:
resolution: {integrity: sha512-sdPRb9K6iL5XZOmBubg8yiFp5yS/JdUDQsq5e6h95km91MCYMuvp7mh1fjPEYUhvHepKpZOjnEaMBR4PxjWDzg==} resolution: {integrity: sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==}
dependencies: dependencies:
'@types/estree': 1.0.1 '@types/estree': 1.0.5
'@types/json-schema': 7.0.12 '@types/json-schema': 7.0.12
/@types/estree-jsx@1.0.3: /@types/estree-jsx@1.0.3:

View File

@ -24,7 +24,7 @@ export const rule = ESLintUtils.RuleCreator(() => __filename)({
defaultOptions: [], defaultOptions: [],
create(context) { create(context) {
// jsonc-eslint-parser adds this property to parserServices where appropriate // 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 {};
} }
return { return {