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/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",

View File

@ -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 {};

View File

@ -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,

View File

@ -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(() => ``)<Options, MessageIds>({
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
);

View File

@ -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<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": {
"@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"
},

View File

@ -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<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(
eslintConfigPath: string | undefined,
options: Schema,

View File

@ -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}",

View File

@ -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<EslintPluginOptions> = [
).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<string>();
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

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
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:

View File

@ -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 {