Torin f1c090b640
fix(linter): update allowed ESLint config file extensions (#30127)
ESLint added experimental support for typescript config files since
[9.9.0](https://eslint.org/blog/2024/08/eslint-v9.9.0-released/#experimental-typescript-configuration-files),
and as of
[9.18.0](https://eslint.org/blog/2025/01/eslint-v9.18.0-released/#stable-typescript-configuration-file-support)
that support is stable. This PR add ts/mts/cts to the list of known
eslint config files, and adds the same extensions to config file
generators

## Current Behavior
When using the eslint executor with a ts file, returns error "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"

When using the eslint plugin, the inferred task is not created for
projects that do not have a non-ts eslint config.

### Workarounds
- Compiling ts rules/configs in a project. Introduces other issues
- Using jiti or comparable
- For plugin users, having a fake eslint.config.js at the root allows
the inferred task to be created. ESLint will still use the ts config.
  - Cache targets are wrong
  - Complications in non-monorepo workspaces

## Expected Behavior
When using the eslint executor with a ts file, no error is thrown.

When using the eslint plugin with a ts file, the inferred task is
created.

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->
No issues, but addresses [this
discussion](https://github.com/nrwl/nx/discussions/29710#discussion-7856165)

---------

Co-authored-by: Leosvel Pérez Espinosa <leosvel.perez.espinosa@gmail.com>
2025-06-09 09:29:19 -04:00

184 lines
4.7 KiB
TypeScript

import 'nx/src/internal-testing-utils/mock-project-graph';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import {
Tree,
readProjectConfiguration,
readJson,
updateJson,
joinPathFragments,
writeJson,
} from '@nx/devkit';
import type { Linter as ESLint } from 'eslint';
import generator from './generator';
import pluginGenerator from '../plugin/plugin';
import generatorGenerator from '../generator/generator';
import executorGenerator from '../executor/executor';
import { PackageJson } from 'nx/src/utils/package-json';
describe('lint-checks generator', () => {
let tree: Tree;
beforeEach(async () => {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
await pluginGenerator(tree, {
directory: 'plugin',
importPath: '@acme/plugin',
compiler: 'tsc',
linter: 'eslint',
skipFormat: false,
skipTsConfig: false,
skipLintChecks: true, // we manually call it s.t. we can update config files first
unitTestRunner: 'jest',
});
await generatorGenerator(tree, {
name: 'my-generator',
path: 'plugin/src/generators/my-generator',
unitTestRunner: 'jest',
skipLintChecks: true,
});
await executorGenerator(tree, {
name: 'my-executor',
path: 'plugin/src/executors/my-executor',
unitTestRunner: 'jest',
includeHasher: false,
skipLintChecks: true,
});
});
it('should update configuration files for default plugin', async () => {
await generator(tree, { projectName: 'plugin' });
const projectConfig = readProjectConfiguration(tree, 'plugin');
const eslintConfig: ESLint.Config = readJson(
tree,
`${projectConfig.root}/.eslintrc.json`
);
expect(eslintConfig.overrides).toContainEqual(
expect.objectContaining({
files: expect.arrayContaining([
'./executors.json',
'./package.json',
'./generators.json',
]),
rules: {
'@nx/nx-plugin-checks': 'error',
},
})
);
});
it('should not duplicate configuration', async () => {
await generator(tree, { projectName: 'plugin' });
await generator(tree, { projectName: 'plugin' });
const projectConfig = readProjectConfiguration(tree, 'plugin');
const eslintConfig: ESLint.Config = readJson(
tree,
`${projectConfig.root}/.eslintrc.json`
);
expect(
eslintConfig.overrides.find((x) => '@nx/nx-plugin-checks' in x.rules)
).toMatchInlineSnapshot(`
{
"files": [
"./package.json",
"./generators.json",
"./executors.json",
],
"parser": "jsonc-eslint-parser",
"rules": {
"@nx/nx-plugin-checks": "error",
},
}
`);
});
it('should update configuration files for angular-style plugin', async () => {
const startingProjectConfig = readProjectConfiguration(tree, 'plugin');
updateJson(
tree,
joinPathFragments(startingProjectConfig.root, 'package.json'),
(json: PackageJson) => {
json.schematics = './collection.json';
delete json.generators;
json.builders = './builders.json';
delete json.executors;
json['ng-update'] = './migrations.json';
return json;
}
);
writeJson(
tree,
joinPathFragments(startingProjectConfig.root, 'migrations.json'),
{}
);
await generator(tree, { projectName: 'plugin' });
const projectConfig = readProjectConfiguration(tree, 'plugin');
const eslintConfig: ESLint.Config = readJson(
tree,
`${projectConfig.root}/.eslintrc.json`
);
expect(eslintConfig.overrides).toMatchInlineSnapshot(`
[
{
"files": [
"*.ts",
"*.tsx",
"*.js",
"*.jsx",
],
"rules": {},
},
{
"files": [
"*.ts",
"*.tsx",
],
"rules": {},
},
{
"files": [
"*.js",
"*.jsx",
],
"rules": {},
},
{
"files": [
"*.json",
],
"parser": "jsonc-eslint-parser",
"rules": {
"@nx/dependency-checks": [
"error",
{
"ignoredFiles": [
"{projectRoot}/eslint.config.{js,cjs,mjs,ts,cts,mts}",
],
},
],
},
},
{
"files": [
"./package.json",
"./collection.json",
"./builders.json",
"./migrations.json",
],
"parser": "jsonc-eslint-parser",
"rules": {
"@nx/nx-plugin-checks": "error",
},
},
]
`);
});
});