feat(linter): support yaml for flat config conversion (#20022)

This commit is contained in:
Miroslav Jonaš 2023-11-22 20:37:51 +01:00 committed by GitHub
parent 31df83bb2f
commit d1a213fa4e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 386 additions and 115 deletions

View File

@ -30,7 +30,8 @@
"executors": "./executors.json", "executors": "./executors.json",
"generators": "./generators.json", "generators": "./generators.json",
"peerDependencies": { "peerDependencies": {
"eslint": "^8.0.0" "eslint": "^8.0.0",
"js-yaml": "4.1.0"
}, },
"dependencies": { "dependencies": {
"tslib": "^2.3.0", "tslib": "^2.3.0",
@ -41,6 +42,9 @@
"peerDependenciesMeta": { "peerDependenciesMeta": {
"eslint": { "eslint": {
"optional": true "optional": true
},
"js-yaml": {
"optional": true
} }
}, },
"publishConfig": { "publishConfig": {

View File

@ -5,10 +5,12 @@ exports[`convert-to-flat-config generator should add env configuration 1`] = `
const nxEslintPlugin = require('@nx/eslint-plugin'); const nxEslintPlugin = require('@nx/eslint-plugin');
const globals = require('globals'); const globals = require('globals');
const js = require('@eslint/js'); const js = require('@eslint/js');
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: __dirname,
recommendedConfig: js.configs.recommended, recommendedConfig: js.configs.recommended,
}); });
module.exports = [ module.exports = [
{ plugins: { '@nx': nxEslintPlugin } }, { plugins: { '@nx': nxEslintPlugin } },
{ languageOptions: { globals: { ...globals.browser, ...globals.node } } }, { languageOptions: { globals: { ...globals.browser, ...globals.node } } },
@ -49,10 +51,12 @@ exports[`convert-to-flat-config generator should add global and env configuratio
const nxEslintPlugin = require('@nx/eslint-plugin'); const nxEslintPlugin = require('@nx/eslint-plugin');
const globals = require('globals'); const globals = require('globals');
const js = require('@eslint/js'); const js = require('@eslint/js');
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: __dirname,
recommendedConfig: js.configs.recommended, recommendedConfig: js.configs.recommended,
}); });
module.exports = [ module.exports = [
{ plugins: { '@nx': nxEslintPlugin } }, { plugins: { '@nx': nxEslintPlugin } },
{ {
@ -96,10 +100,12 @@ exports[`convert-to-flat-config generator should add global configuration 1`] =
"const { FlatCompat } = require('@eslint/eslintrc'); "const { FlatCompat } = require('@eslint/eslintrc');
const nxEslintPlugin = require('@nx/eslint-plugin'); const nxEslintPlugin = require('@nx/eslint-plugin');
const js = require('@eslint/js'); const js = require('@eslint/js');
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: __dirname,
recommendedConfig: js.configs.recommended, recommendedConfig: js.configs.recommended,
}); });
module.exports = [ module.exports = [
{ plugins: { '@nx': nxEslintPlugin } }, { plugins: { '@nx': nxEslintPlugin } },
{ languageOptions: { globals: { myCustomGlobal: 'readonly' } } }, { languageOptions: { globals: { myCustomGlobal: 'readonly' } } },
@ -139,10 +145,12 @@ exports[`convert-to-flat-config generator should add global eslintignores 1`] =
"const { FlatCompat } = require('@eslint/eslintrc'); "const { FlatCompat } = require('@eslint/eslintrc');
const nxEslintPlugin = require('@nx/eslint-plugin'); const nxEslintPlugin = require('@nx/eslint-plugin');
const js = require('@eslint/js'); const js = require('@eslint/js');
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: __dirname,
recommendedConfig: js.configs.recommended, recommendedConfig: js.configs.recommended,
}); });
module.exports = [ module.exports = [
{ plugins: { '@nx': nxEslintPlugin } }, { plugins: { '@nx': nxEslintPlugin } },
{ {
@ -183,10 +191,12 @@ exports[`convert-to-flat-config generator should add parser 1`] = `
const nxEslintPlugin = require('@nx/eslint-plugin'); const nxEslintPlugin = require('@nx/eslint-plugin');
const typescriptEslintParser = require('@typescript-eslint/parser'); const typescriptEslintParser = require('@typescript-eslint/parser');
const js = require('@eslint/js'); const js = require('@eslint/js');
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: __dirname,
recommendedConfig: js.configs.recommended, recommendedConfig: js.configs.recommended,
}); });
module.exports = [ module.exports = [
{ plugins: { '@nx': nxEslintPlugin } }, { plugins: { '@nx': nxEslintPlugin } },
{ languageOptions: { parser: typescriptEslintParser } }, { languageOptions: { parser: typescriptEslintParser } },
@ -229,10 +239,12 @@ const eslintPluginSingleName = require('eslint-plugin-single-name');
const scopeEslintPluginWithName = require('@scope/eslint-plugin-with-name'); const scopeEslintPluginWithName = require('@scope/eslint-plugin-with-name');
const justScopeEslintPlugin = require('@just-scope/eslint-plugin'); const justScopeEslintPlugin = require('@just-scope/eslint-plugin');
const js = require('@eslint/js'); const js = require('@eslint/js');
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: __dirname,
recommendedConfig: js.configs.recommended, recommendedConfig: js.configs.recommended,
}); });
module.exports = [ module.exports = [
{ {
plugins: { plugins: {
@ -278,10 +290,12 @@ exports[`convert-to-flat-config generator should add settings 1`] = `
"const { FlatCompat } = require('@eslint/eslintrc'); "const { FlatCompat } = require('@eslint/eslintrc');
const nxEslintPlugin = require('@nx/eslint-plugin'); const nxEslintPlugin = require('@nx/eslint-plugin');
const js = require('@eslint/js'); const js = require('@eslint/js');
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: __dirname,
recommendedConfig: js.configs.recommended, recommendedConfig: js.configs.recommended,
}); });
module.exports = [ module.exports = [
{ plugins: { '@nx': nxEslintPlugin } }, { plugins: { '@nx': nxEslintPlugin } },
{ settings: { sharedData: 'Hello' } }, { settings: { sharedData: 'Hello' } },
@ -317,41 +331,16 @@ module.exports = [
" "
`; `;
exports[`convert-to-flat-config generator should handle custom eslintignores 1`] = ` exports[`convert-to-flat-config generator should convert json successfully 1`] = `
"const baseConfig = require('../../eslint.config.js');
module.exports = [
...baseConfig,
{
files: [
'libs/test-lib/**/*.ts',
'libs/test-lib/**/*.tsx',
'libs/test-lib/**/*.js',
'libs/test-lib/**/*.jsx',
],
rules: {},
},
{
files: ['libs/test-lib/**/*.ts', 'libs/test-lib/**/*.tsx'],
rules: {},
},
{
files: ['libs/test-lib/**/*.js', 'libs/test-lib/**/*.jsx'],
rules: {},
},
{ ignores: ['libs/test-lib/ignore/me'] },
{ ignores: ['libs/test-lib/ignore/me/as/well'] },
];
"
`;
exports[`convert-to-flat-config generator should run successfully 1`] = `
"const { FlatCompat } = require('@eslint/eslintrc'); "const { FlatCompat } = require('@eslint/eslintrc');
const nxEslintPlugin = require('@nx/eslint-plugin'); const nxEslintPlugin = require('@nx/eslint-plugin');
const js = require('@eslint/js'); const js = require('@eslint/js');
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: __dirname,
recommendedConfig: js.configs.recommended, recommendedConfig: js.configs.recommended,
}); });
module.exports = [ module.exports = [
{ plugins: { '@nx': nxEslintPlugin } }, { plugins: { '@nx': nxEslintPlugin } },
{ {
@ -386,8 +375,9 @@ module.exports = [
" "
`; `;
exports[`convert-to-flat-config generator should run successfully 2`] = ` exports[`convert-to-flat-config generator should convert json successfully 2`] = `
"const baseConfig = require('../../eslint.config.js'); "const baseConfig = require('../../eslint.config.js');
module.exports = [ module.exports = [
...baseConfig, ...baseConfig,
{ {
@ -410,3 +400,171 @@ module.exports = [
]; ];
" "
`; `;
exports[`convert-to-flat-config generator should convert yaml successfully 1`] = `
"const { FlatCompat } = require('@eslint/eslintrc');
const nxEslintPlugin = require('@nx/eslint-plugin');
const js = require('@eslint/js');
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
});
module.exports = [
{ plugins: { '@nx': nxEslintPlugin } },
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
rules: {
'@nx/enforce-module-boundaries': [
'error',
{
enforceBuildableLibDependency: true,
allow: [],
depConstraints: [
{
sourceTag: '*',
onlyDependOnLibsWithTags: ['*'],
},
],
},
],
},
},
...compat.config({ extends: ['plugin:@nx/typescript'] }).map((config) => ({
...config,
files: ['**/*.ts', '**/*.tsx'],
rules: {},
})),
...compat.config({ extends: ['plugin:@nx/javascript'] }).map((config) => ({
...config,
files: ['**/*.js', '**/*.jsx'],
rules: {},
})),
];
"
`;
exports[`convert-to-flat-config generator should convert yaml successfully 2`] = `
"const baseConfig = require('../../eslint.config.js');
module.exports = [
...baseConfig,
{
files: [
'libs/test-lib/**/*.ts',
'libs/test-lib/**/*.tsx',
'libs/test-lib/**/*.js',
'libs/test-lib/**/*.jsx',
],
rules: {},
},
{
files: ['libs/test-lib/**/*.ts', 'libs/test-lib/**/*.tsx'],
rules: {},
},
{
files: ['libs/test-lib/**/*.js', 'libs/test-lib/**/*.jsx'],
rules: {},
},
];
"
`;
exports[`convert-to-flat-config generator should convert yml successfully 1`] = `
"const { FlatCompat } = require('@eslint/eslintrc');
const nxEslintPlugin = require('@nx/eslint-plugin');
const js = require('@eslint/js');
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
});
module.exports = [
{ plugins: { '@nx': nxEslintPlugin } },
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
rules: {
'@nx/enforce-module-boundaries': [
'error',
{
enforceBuildableLibDependency: true,
allow: [],
depConstraints: [
{
sourceTag: '*',
onlyDependOnLibsWithTags: ['*'],
},
],
},
],
},
},
...compat.config({ extends: ['plugin:@nx/typescript'] }).map((config) => ({
...config,
files: ['**/*.ts', '**/*.tsx'],
rules: {},
})),
...compat.config({ extends: ['plugin:@nx/javascript'] }).map((config) => ({
...config,
files: ['**/*.js', '**/*.jsx'],
rules: {},
})),
];
"
`;
exports[`convert-to-flat-config generator should convert yml successfully 2`] = `
"const baseConfig = require('../../eslint.config.js');
module.exports = [
...baseConfig,
{
files: [
'libs/test-lib/**/*.ts',
'libs/test-lib/**/*.tsx',
'libs/test-lib/**/*.js',
'libs/test-lib/**/*.jsx',
],
rules: {},
},
{
files: ['libs/test-lib/**/*.ts', 'libs/test-lib/**/*.tsx'],
rules: {},
},
{
files: ['libs/test-lib/**/*.js', 'libs/test-lib/**/*.jsx'],
rules: {},
},
];
"
`;
exports[`convert-to-flat-config generator should handle custom eslintignores 1`] = `
"const baseConfig = require('../../eslint.config.js');
module.exports = [
...baseConfig,
{
files: [
'libs/test-lib/**/*.ts',
'libs/test-lib/**/*.tsx',
'libs/test-lib/**/*.js',
'libs/test-lib/**/*.jsx',
],
rules: {},
},
{
files: ['libs/test-lib/**/*.ts', 'libs/test-lib/**/*.tsx'],
rules: {},
},
{
files: ['libs/test-lib/**/*.js', 'libs/test-lib/**/*.jsx'],
rules: {},
},
{ ignores: ['libs/test-lib/ignore/me'] },
{ ignores: ['libs/test-lib/ignore/me/as/well'] },
];
"
`;

View File

@ -1,4 +1,4 @@
import { Tree } from '@nx/devkit'; import { Tree, readJson } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { convertEslintJsonToFlatConfig } from './json-converter'; import { convertEslintJsonToFlatConfig } from './json-converter';
@ -58,22 +58,23 @@ describe('convertEslintJsonToFlatConfig', () => {
tree.write('.eslintignore', 'node_modules\nsomething/else'); tree.write('.eslintignore', 'node_modules\nsomething/else');
convertEslintJsonToFlatConfig( const { content } = convertEslintJsonToFlatConfig(
tree, tree,
'', '',
'.eslintrc.json', readJson(tree, '.eslintrc.json'),
'eslint.config.js',
['.eslintignore'] ['.eslintignore']
); );
expect(tree.read('eslint.config.js', 'utf-8')).toMatchInlineSnapshot(` expect(content).toMatchInlineSnapshot(`
"const { FlatCompat } = require("@eslint/eslintrc"); "const { FlatCompat } = require("@eslint/eslintrc");
const nxEslintPlugin = require("@nx/eslint-plugin"); const nxEslintPlugin = require("@nx/eslint-plugin");
const js = require("@eslint/js"); const js = require("@eslint/js");
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: __dirname,
recommendedConfig: js.configs.recommended, recommendedConfig: js.configs.recommended,
}); });
module.exports = [ module.exports = [
{ plugins: { "@nx": nxEslintPlugin } }, { plugins: { "@nx": nxEslintPlugin } },
{ {
@ -118,8 +119,6 @@ describe('convertEslintJsonToFlatConfig', () => {
]; ];
" "
`); `);
expect(tree.exists('.eslintrc.json')).toBeFalsy();
}); });
it('should convert project configs', async () => { it('should convert project configs', async () => {
@ -170,23 +169,24 @@ describe('convertEslintJsonToFlatConfig', () => {
tree.write('mylib/.eslintignore', 'node_modules\nsomething/else'); tree.write('mylib/.eslintignore', 'node_modules\nsomething/else');
convertEslintJsonToFlatConfig( const { content } = convertEslintJsonToFlatConfig(
tree, tree,
'mylib', 'mylib',
'.eslintrc.json', readJson(tree, 'mylib/.eslintrc.json'),
'eslint.config.js',
['mylib/.eslintignore'] ['mylib/.eslintignore']
); );
expect(tree.read('mylib/eslint.config.js', 'utf-8')).toMatchInlineSnapshot(` expect(content).toMatchInlineSnapshot(`
"const { FlatCompat } = require("@eslint/eslintrc"); "const { FlatCompat } = require("@eslint/eslintrc");
const baseConfig = require("../../eslint.config.js"); const baseConfig = require("../../eslint.config.js");
const globals = require("globals"); const globals = require("globals");
const js = require("@eslint/js"); const js = require("@eslint/js");
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: __dirname,
recommendedConfig: js.configs.recommended, recommendedConfig: js.configs.recommended,
}); });
module.exports = [ module.exports = [
...baseConfig, ...baseConfig,
...compat.extends("plugin:@nx/react-typescript", "next", "next/core-web-vitals"), ...compat.extends("plugin:@nx/react-typescript", "next", "next/core-web-vitals"),
@ -228,7 +228,5 @@ describe('convertEslintJsonToFlatConfig', () => {
]; ];
" "
`); `);
expect(tree.exists('mylib/.eslintrc.json')).toBeFalsy();
}); });
}); });

View File

@ -1,13 +1,6 @@
import { import { Tree, names } from '@nx/devkit';
Tree,
addDependenciesToPackageJson,
names,
readJson,
} from '@nx/devkit';
import { join } from 'path';
import { ESLint } from 'eslint'; import { ESLint } from 'eslint';
import * as ts from 'typescript'; import * as ts from 'typescript';
import { eslintVersion, eslintrcVersion } from '../../../utils/versions';
import { import {
createNodeList, createNodeList,
generateAst, generateAst,
@ -26,21 +19,20 @@ import { getPluginImport } from '../../utils/eslint-file';
export function convertEslintJsonToFlatConfig( export function convertEslintJsonToFlatConfig(
tree: Tree, tree: Tree,
root: string, root: string,
sourceFile: string, config: ESLint.ConfigData,
destinationFile: string,
ignorePaths: string[] ignorePaths: string[]
) { ): { content: string; addESLintRC: boolean; addESLintJS: boolean } {
const importsMap = new Map<string, string>(); const importsMap = new Map<string, string>();
const exportElements: ts.Expression[] = []; const exportElements: ts.Expression[] = [];
let isFlatCompatNeeded = false; let isFlatCompatNeeded = false;
let isESLintJSNeeded = false;
let combinedConfig: ts.PropertyAssignment[] = []; let combinedConfig: ts.PropertyAssignment[] = [];
let languageOptions: ts.PropertyAssignment[] = []; let languageOptions: ts.PropertyAssignment[] = [];
// read original config
const config: ESLint.ConfigData = readJson(tree, `${root}/${sourceFile}`);
if (config.extends) { if (config.extends) {
isFlatCompatNeeded = addExtends(importsMap, exportElements, config, tree); const extendsResult = addExtends(importsMap, exportElements, config);
isFlatCompatNeeded = extendsResult.isFlatCompatNeeded;
isESLintJSNeeded = extendsResult.isESLintJSNeeded;
} }
if (config.plugins) { if (config.plugins) {
@ -188,36 +180,28 @@ export function convertEslintJsonToFlatConfig(
} }
} }
tree.delete(join(root, sourceFile));
// create the node list and print it to new file // create the node list and print it to new file
const nodeList = createNodeList( const nodeList = createNodeList(
importsMap, importsMap,
exportElements, exportElements,
isFlatCompatNeeded isFlatCompatNeeded
); );
const content = stringifyNodeList(nodeList, root, destinationFile);
tree.write(join(root, destinationFile), content);
if (isFlatCompatNeeded) { return {
addDependenciesToPackageJson( content: stringifyNodeList(nodeList, root),
tree, addESLintRC: isFlatCompatNeeded,
{}, addESLintJS: isESLintJSNeeded,
{ };
'@eslint/eslintrc': eslintrcVersion,
}
);
}
} }
// add parsed extends to export blocks and add import statements // add parsed extends to export blocks and add import statements
function addExtends( function addExtends(
importsMap: Map<string, string | string[]>, importsMap: Map<string, string | string[]>,
configBlocks: ts.Expression[], configBlocks: ts.Expression[],
config: ESLint.ConfigData, config: ESLint.ConfigData
tree: Tree ): { isFlatCompatNeeded: boolean; isESLintJSNeeded: boolean } {
): boolean {
let isFlatCompatNeeded = false; let isFlatCompatNeeded = false;
let isESLintJSNeeded = false;
const extendsConfig = Array.isArray(config.extends) const extendsConfig = Array.isArray(config.extends)
? config.extends ? config.extends
: [config.extends]; : [config.extends];
@ -253,13 +237,7 @@ function addExtends(
}); });
if (eslintPluginExtends.length) { if (eslintPluginExtends.length) {
addDependenciesToPackageJson( isESLintJSNeeded = true;
tree,
{},
{
'@eslint/js': eslintVersion,
}
);
importsMap.set('@eslint/js', 'js'); importsMap.set('@eslint/js', 'js');
eslintPluginExtends.forEach((plugin) => { eslintPluginExtends.forEach((plugin) => {
@ -277,18 +255,12 @@ function addExtends(
} }
if (eslintrcConfigs.length) { if (eslintrcConfigs.length) {
isFlatCompatNeeded = true; isFlatCompatNeeded = true;
addDependenciesToPackageJson( isESLintJSNeeded = true;
tree,
{},
{
'@eslint/js': eslintVersion,
}
);
configBlocks.push(generatePluginExtendsElement(eslintrcConfigs)); configBlocks.push(generatePluginExtendsElement(eslintrcConfigs));
} }
return isFlatCompatNeeded; return { isFlatCompatNeeded, isESLintJSNeeded };
} }
function addPlugins( function addPlugins(

View File

@ -12,7 +12,7 @@ import { ConvertToFlatConfigGeneratorSchema } from './schema';
import { lintProjectGenerator } from '../lint-project/lint-project'; import { lintProjectGenerator } from '../lint-project/lint-project';
import { Linter } from '../utils/linter'; import { Linter } from '../utils/linter';
import { eslintrcVersion } from '../../utils/versions'; import { eslintrcVersion } from '../../utils/versions';
import { read } from 'fs'; import { dump } from 'js-yaml';
describe('convert-to-flat-config generator', () => { describe('convert-to-flat-config generator', () => {
let tree: Tree; let tree: Tree;
@ -42,7 +42,7 @@ describe('convert-to-flat-config generator', () => {
}); });
}); });
it('should run successfully', async () => { it('should convert json successfully', async () => {
await lintProjectGenerator(tree, { await lintProjectGenerator(tree, {
skipFormat: false, skipFormat: false,
linter: Linter.EsLint, linter: Linter.EsLint,
@ -68,6 +68,66 @@ describe('convert-to-flat-config generator', () => {
); );
}); });
it('should convert yaml successfully', async () => {
await lintProjectGenerator(tree, {
skipFormat: false,
linter: Linter.EsLint,
eslintFilePatterns: ['**/*.ts'],
project: 'test-lib',
setParserOptionsProject: false,
});
const yamlContent = dump(readJson(tree, 'libs/test-lib/.eslintrc.json'));
tree.delete('libs/test-lib/.eslintrc.json');
tree.write('libs/test-lib/.eslintrc.yaml', yamlContent);
await convertToFlatConfigGenerator(tree, options);
expect(tree.exists('eslint.config.js')).toBeTruthy();
expect(tree.read('eslint.config.js', 'utf-8')).toMatchSnapshot();
expect(tree.exists('libs/test-lib/eslint.config.js')).toBeTruthy();
expect(
tree.read('libs/test-lib/eslint.config.js', 'utf-8')
).toMatchSnapshot();
// check nx.json changes
const nxJson = readJson(tree, 'nx.json');
expect(nxJson.targetDefaults.lint.inputs).toContain(
'{workspaceRoot}/eslint.config.js'
);
expect(nxJson.namedInputs.production).toContain(
'!{projectRoot}/eslint.config.js'
);
});
it('should convert yml successfully', async () => {
await lintProjectGenerator(tree, {
skipFormat: false,
linter: Linter.EsLint,
eslintFilePatterns: ['**/*.ts'],
project: 'test-lib',
setParserOptionsProject: false,
});
const yamlContent = dump(readJson(tree, 'libs/test-lib/.eslintrc.json'));
tree.delete('libs/test-lib/.eslintrc.json');
tree.write('libs/test-lib/.eslintrc.yml', yamlContent);
await convertToFlatConfigGenerator(tree, options);
expect(tree.exists('eslint.config.js')).toBeTruthy();
expect(tree.read('eslint.config.js', 'utf-8')).toMatchSnapshot();
expect(tree.exists('libs/test-lib/eslint.config.js')).toBeTruthy();
expect(
tree.read('libs/test-lib/eslint.config.js', 'utf-8')
).toMatchSnapshot();
// check nx.json changes
const nxJson = readJson(tree, 'nx.json');
expect(nxJson.targetDefaults.lint.inputs).toContain(
'{workspaceRoot}/eslint.config.js'
);
expect(nxJson.namedInputs.production).toContain(
'!{projectRoot}/eslint.config.js'
);
});
it('should add plugin extends', async () => { it('should add plugin extends', async () => {
await lintProjectGenerator(tree, { await lintProjectGenerator(tree, {
skipFormat: false, skipFormat: false,
@ -86,10 +146,12 @@ describe('convert-to-flat-config generator', () => {
"const { FlatCompat } = require('@eslint/eslintrc'); "const { FlatCompat } = require('@eslint/eslintrc');
const nxEslintPlugin = require('@nx/eslint-plugin'); const nxEslintPlugin = require('@nx/eslint-plugin');
const js = require('@eslint/js'); const js = require('@eslint/js');
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: __dirname,
recommendedConfig: js.configs.recommended, recommendedConfig: js.configs.recommended,
}); });
module.exports = [ module.exports = [
...compat.extends('plugin:storybook/recommended'), ...compat.extends('plugin:storybook/recommended'),
{ plugins: { '@nx': nxEslintPlugin } }, { plugins: { '@nx': nxEslintPlugin } },
@ -127,6 +189,7 @@ describe('convert-to-flat-config generator', () => {
expect(tree.read('libs/test-lib/eslint.config.js', 'utf-8')) expect(tree.read('libs/test-lib/eslint.config.js', 'utf-8'))
.toMatchInlineSnapshot(` .toMatchInlineSnapshot(`
"const baseConfig = require('../../eslint.config.js'); "const baseConfig = require('../../eslint.config.js');
module.exports = [ module.exports = [
...baseConfig, ...baseConfig,
{ {
@ -337,10 +400,12 @@ describe('convert-to-flat-config generator', () => {
"const { FlatCompat } = require('@eslint/eslintrc'); "const { FlatCompat } = require('@eslint/eslintrc');
const nxEslintPlugin = require('@nx/eslint-plugin'); const nxEslintPlugin = require('@nx/eslint-plugin');
const js = require('@eslint/js'); const js = require('@eslint/js');
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: __dirname,
recommendedConfig: js.configs.recommended, recommendedConfig: js.configs.recommended,
}); });
module.exports = [ module.exports = [
{ plugins: { '@nx': nxEslintPlugin } }, { plugins: { '@nx': nxEslintPlugin } },
{ {

View File

@ -1,8 +1,10 @@
import { import {
addDependenciesToPackageJson,
formatFiles, formatFiles,
getProjects, getProjects,
NxJsonConfiguration, NxJsonConfiguration,
ProjectConfiguration, ProjectConfiguration,
readJson,
readNxJson, readNxJson,
Tree, Tree,
updateJson, updateJson,
@ -10,7 +12,11 @@ import {
} from '@nx/devkit'; } from '@nx/devkit';
import { ConvertToFlatConfigGeneratorSchema } from './schema'; import { ConvertToFlatConfigGeneratorSchema } from './schema';
import { findEslintFile } from '../utils/eslint-file'; import { findEslintFile } from '../utils/eslint-file';
import { join } from 'path';
import { eslintrcVersion, eslintVersion } from '../../utils/versions';
import { ESLint } from 'eslint';
import { convertEslintJsonToFlatConfig } from './converters/json-converter'; import { convertEslintJsonToFlatConfig } from './converters/json-converter';
import { load } from 'js-yaml';
export async function convertToFlatConfigGenerator( export async function convertToFlatConfigGenerator(
tree: Tree, tree: Tree,
@ -20,9 +26,9 @@ export async function convertToFlatConfigGenerator(
if (!eslintFile) { if (!eslintFile) {
throw new Error('Could not find root eslint file'); throw new Error('Could not find root eslint file');
} }
if (!eslintFile.endsWith('.json')) { if (eslintFile.endsWith('.js')) {
throw new Error( throw new Error(
'Only json eslint config files are supported for conversion' 'Only json and yaml eslint config files are supported for conversion'
); );
} }
@ -60,15 +66,15 @@ export async function convertToFlatConfigGenerator(
export default convertToFlatConfigGenerator; export default convertToFlatConfigGenerator;
function convertRootToFlatConfig(tree: Tree, eslintFile: string) { function convertRootToFlatConfig(tree: Tree, eslintFile: string) {
if (eslintFile.endsWith('.base.json')) { if (/\.base\.(js|json|yml|yaml)$/.test(eslintFile)) {
convertConfigToFlatConfig( convertConfigToFlatConfig(tree, '', eslintFile, 'eslint.base.config.js');
tree,
'',
'.eslintrc.base.json',
'eslint.base.config.js'
);
} }
convertConfigToFlatConfig(tree, '', '.eslintrc.json', 'eslint.config.js'); convertConfigToFlatConfig(
tree,
'',
eslintFile.replace('.base.', '.'),
'eslint.config.js'
);
} }
function convertProjectToFlatConfig( function convertProjectToFlatConfig(
@ -78,7 +84,8 @@ function convertProjectToFlatConfig(
nxJson: NxJsonConfiguration, nxJson: NxJsonConfiguration,
eslintIgnoreFiles: Set<string> eslintIgnoreFiles: Set<string>
) { ) {
if (tree.exists(`${projectConfig.root}/.eslintrc.json`)) { const eslintFile = findEslintFile(tree, projectConfig.root);
if (eslintFile && !eslintFile.endsWith('.js')) {
if (projectConfig.targets) { if (projectConfig.targets) {
const eslintTargets = Object.keys(projectConfig.targets || {}).filter( const eslintTargets = Object.keys(projectConfig.targets || {}).filter(
(t) => projectConfig.targets[t].executor === '@nx/eslint:lint' (t) => projectConfig.targets[t].executor === '@nx/eslint:lint'
@ -105,7 +112,7 @@ function convertProjectToFlatConfig(
convertConfigToFlatConfig( convertConfigToFlatConfig(
tree, tree,
projectConfig.root, projectConfig.root,
'.eslintrc.json', eslintFile,
'eslint.config.js', 'eslint.config.js',
ignorePath ignorePath
); );
@ -153,5 +160,67 @@ function convertConfigToFlatConfig(
const ignorePaths = ignorePath const ignorePaths = ignorePath
? [ignorePath, `${root}/.eslintignore`] ? [ignorePath, `${root}/.eslintignore`]
: [`${root}/.eslintignore`]; : [`${root}/.eslintignore`];
convertEslintJsonToFlatConfig(tree, root, source, target, ignorePaths);
if (source.endsWith('.json')) {
const config: ESLint.ConfigData = readJson(tree, `${root}/${source}`);
const conversionResult = convertEslintJsonToFlatConfig(
tree,
root,
config,
ignorePaths
);
return processConvertedConfig(tree, root, source, target, conversionResult);
}
if (source.endsWith('.yaml') || source.endsWith('.yml')) {
const originalContent = tree.read(`${root}/${source}`, 'utf-8');
const config = load(originalContent, {
json: true,
filename: source,
}) as ESLint.ConfigData;
const conversionResult = convertEslintJsonToFlatConfig(
tree,
root,
config,
ignorePaths
);
return processConvertedConfig(tree, root, source, target, conversionResult);
}
}
function processConvertedConfig(
tree: Tree,
root: string,
source: string,
target: string,
{
content,
addESLintRC,
addESLintJS,
}: { content: string; addESLintRC: boolean; addESLintJS: boolean }
) {
// remove original config file
tree.delete(join(root, source));
// save new
tree.write(join(root, target), content);
// add missing packages
if (addESLintRC) {
addDependenciesToPackageJson(
tree,
{},
{
'@eslint/eslintrc': eslintrcVersion,
}
);
}
if (addESLintJS) {
addDependenciesToPackageJson(
tree,
{},
{
'@eslint/js': eslintVersion,
}
);
}
} }

View File

@ -7,7 +7,6 @@ import {
generateFlatOverride, generateFlatOverride,
stringifyNodeList, stringifyNodeList,
} from '../utils/flat-config/ast-utils'; } from '../utils/flat-config/ast-utils';
import { addPluginsToLintConfig } from '../utils/eslint-file';
/** /**
* This configuration is intended to apply to all TypeScript source files. * This configuration is intended to apply to all TypeScript source files.
@ -97,7 +96,7 @@ export const getGlobalFlatEslintConfiguration = (
rootProject?: boolean rootProject?: boolean
): string => { ): string => {
const nodeList = createNodeList(new Map(), [], true); const nodeList = createNodeList(new Map(), [], true);
let content = stringifyNodeList(nodeList, '', 'eslint.config.js'); let content = stringifyNodeList(nodeList, '');
content = addImportToFlatConfig(content, 'nxPlugin', '@nx/eslint-plugin'); content = addImportToFlatConfig(content, 'nxPlugin', '@nx/eslint-plugin');
content = addPluginsToExportsBlock(content, [ content = addPluginsToExportsBlock(content, [
{ name: '@nx', varName: 'nxPlugin', imp: '@nx/eslint-plugin' }, { name: '@nx', varName: 'nxPlugin', imp: '@nx/eslint-plugin' },

View File

@ -209,11 +209,7 @@ function createEsLintConfiguration(
nodes.push(generateFlatOverride(override, projectConfig.root)); nodes.push(generateFlatOverride(override, projectConfig.root));
}); });
const nodeList = createNodeList(importMap, nodes, isCompatNeeded); const nodeList = createNodeList(importMap, nodes, isCompatNeeded);
const content = stringifyNodeList( const content = stringifyNodeList(nodeList, projectConfig.root);
nodeList,
projectConfig.root,
'eslint.config.js'
);
tree.write(join(projectConfig.root, 'eslint.config.js'), content); tree.write(join(projectConfig.root, 'eslint.config.js'), content);
} else { } else {
writeJson(tree, join(projectConfig.root, `.eslintrc.json`), { writeJson(tree, join(projectConfig.root, `.eslintrc.json`), {

View File

@ -695,18 +695,27 @@ export function stringifyNodeList(
| ts.ExpressionStatement | ts.ExpressionStatement
| ts.SourceFile | ts.SourceFile
>, >,
root: string, root: string
fileName: string
): string { ): string {
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
const resultFile = ts.createSourceFile( const resultFile = ts.createSourceFile(
joinPathFragments(root, fileName), joinPathFragments(root, ''),
'', '',
ts.ScriptTarget.Latest, ts.ScriptTarget.Latest,
true, true,
ts.ScriptKind.JS ts.ScriptKind.JS
); );
return printer.printList(ts.ListFormat.MultiLine, nodes, resultFile); return (
printer
.printList(ts.ListFormat.MultiLine, nodes, resultFile)
// add new line before compat initialization
.replace(
/const compat = new FlatCompat/,
'\nconst compat = new FlatCompat'
)
// add new line before module.exports = ...
.replace(/module\.exports/, '\nmodule.exports')
);
} }
/** /**

View File

@ -124,6 +124,7 @@ describe('updateEslint', () => {
"const FlatCompat = require("@eslint/eslintrc"); "const FlatCompat = require("@eslint/eslintrc");
const js = require("@eslint/js"); const js = require("@eslint/js");
const baseConfig = require("../eslint.config.js"); const baseConfig = require("../eslint.config.js");
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: __dirname,
recommendedConfig: js.configs.recommended, recommendedConfig: js.configs.recommended,