feat(core): Add ESM support for Eslint config file (#29613)

This pull request includes changes to migrate ESLint configuration files
from CommonJS (`.cjs`) to ECMAScript modules (`.mjs`) as the default.

### ESLint Configuration Generation Changes

The changes also ensure consistent generated eslint configs based on the
base eslint config.
- If the workspace root has an `eslint.config.cjs` or `eslint.config.js`
with `module.exports`. When you create a library or application it will
generate an accompanying config at path
`{projectRoot}/eslint.config.cjs` of the same format.
- If the workspace root has an `eslint.config.mjs` or
`eslint.config.mjs` with `export default`. When you create a library or
application it will generate an accompanying config at path
`{projectRoot}/eslint.config.mjs`.
- If no eslint config is found at the workspace root one will be created
`eslint.config.mjs`
This commit is contained in:
Nicholas Cunningham 2025-01-17 11:39:45 -07:00 committed by GitHub
parent a468d72c7f
commit dec21662b6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 4332 additions and 1312 deletions

View File

@ -39,7 +39,7 @@ describe('Move Angular Project', () => {
expect(moveOutput).toContain(`CREATE ${newPath}/tsconfig.app.json`);
expect(moveOutput).toContain(`CREATE ${newPath}/tsconfig.json`);
expect(moveOutput).toContain(`CREATE ${newPath}/tsconfig.spec.json`);
expect(moveOutput).toContain(`CREATE ${newPath}/eslint.config.cjs`);
expect(moveOutput).toContain(`CREATE ${newPath}/eslint.config.mjs`);
expect(moveOutput).toContain(`CREATE ${newPath}/public/favicon.ico`);
expect(moveOutput).toContain(`CREATE ${newPath}/src/index.html`);
expect(moveOutput).toContain(`CREATE ${newPath}/src/main.ts`);

View File

@ -164,13 +164,13 @@ describe('Angular Projects', () => {
it('should lint correctly with eslint and handle external HTML files and inline templates', async () => {
// disable the prefer-standalone rule for app1 which is not standalone
let app1EslintConfig = readFile(`${app1}/eslint.config.cjs`);
let app1EslintConfig = readFile(`${app1}/eslint.config.mjs`);
app1EslintConfig = app1EslintConfig.replace(
`'@angular-eslint/directive-selector': [`,
`'@angular-eslint/prefer-standalone': 'off',
'@angular-eslint/directive-selector': [`
);
updateFile(`${app1}/eslint.config.cjs`, app1EslintConfig);
updateFile(`${app1}/eslint.config.mjs`, app1EslintConfig);
// check apps and lib pass linting for initial generated code
runCLI(`run-many --target lint --projects=${app1},${lib1} --parallel`);

View File

@ -150,10 +150,10 @@ describe('Linter (legacy)', () => {
env: { NX_ADD_PLUGINS: 'false' },
});
checkFilesExist(
'eslint.config.cjs',
`apps/${myapp}/eslint.config.cjs`,
`libs/${mylib}/eslint.config.cjs`,
`libs/${mylib2}/eslint.config.cjs`
'eslint.config.mjs',
`apps/${myapp}/eslint.config.mjs`,
`libs/${mylib}/eslint.config.mjs`,
`libs/${mylib2}/eslint.config.mjs`
);
checkFilesDoNotExist(
'.eslintrc.json',
@ -164,12 +164,12 @@ describe('Linter (legacy)', () => {
// move eslint.config one step up
// to test the absence of the flat eslint config in the project root folder
renameFile(`libs/${mylib2}/eslint.config.cjs`, `libs/eslint.config.cjs`);
renameFile(`libs/${mylib2}/eslint.config.mjs`, `libs/eslint.config.mjs`);
updateFile(
`libs/eslint.config.cjs`,
readFile(`libs/eslint.config.cjs`).replace(
`../../eslint.config.cjs`,
`../eslint.config.cjs`
`libs/eslint.config.mjs`,
readFile(`libs/eslint.config.mjs`).replace(
`../../eslint.config.mjs`,
`../eslint.config.mjs`
)
);
@ -202,9 +202,9 @@ describe('Linter (legacy)', () => {
env: { NX_ADD_PLUGINS: 'false' },
});
checkFilesExist(
'eslint.config.cjs',
`${mylib}/eslint.config.cjs`,
'eslint.base.config.cjs'
'eslint.config.mjs',
`${mylib}/eslint.config.mjs`,
'eslint.base.config.mjs'
);
checkFilesDoNotExist(
'.eslintrc.json',

View File

@ -615,8 +615,8 @@ describe('Linter', () => {
runCLI(`generate @nx/js:lib ${jsLib} --linter eslint`);
checkFilesExist(
`${reactLib}/eslint.config.cjs`,
`${jsLib}/eslint.config.cjs`
`${reactLib}/eslint.config.mjs`,
`${jsLib}/eslint.config.mjs`
);
checkFilesDoNotExist(
`${reactLib}/.eslintrc.json`,

View File

@ -26,7 +26,7 @@ exports[`Extra Nx Misc Tests task graph inputs should correctly expand dependent
],
"lib-base-123": [
"libs/lib-base-123/README.md",
"libs/lib-base-123/eslint.config.cjs",
"libs/lib-base-123/eslint.config.mjs",
"libs/lib-base-123/jest.config.ts",
"libs/lib-base-123/package.json",
"libs/lib-base-123/project.json",
@ -39,7 +39,7 @@ exports[`Extra Nx Misc Tests task graph inputs should correctly expand dependent
],
"lib-dependent-123": [
"libs/lib-dependent-123/README.md",
"libs/lib-dependent-123/eslint.config.cjs",
"libs/lib-dependent-123/eslint.config.mjs",
"libs/lib-dependent-123/jest.config.ts",
"libs/lib-dependent-123/package.json",
"libs/lib-dependent-123/project.json",

View File

@ -94,6 +94,7 @@ exports[`workspace move to nx layout should create nx.json 1`] = `
"!{projectRoot}/karma.conf.js",
"!{projectRoot}/.eslintrc.json",
"!{projectRoot}/eslint.config.cjs",
"!{projectRoot}/eslint.config.mjs",
],
"sharedGlobals": [],
},
@ -104,7 +105,7 @@ exports[`workspace move to nx layout should create nx.json 1`] = `
"default",
"{workspaceRoot}/.eslintrc.json",
"{workspaceRoot}/.eslintignore",
"{workspaceRoot}/eslint.config.cjs",
"{workspaceRoot}/eslint.config.mjs",
],
},
"build": {

View File

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`convert-to-flat-config generator should add env configuration 1`] = `
exports[`convert-to-flat-config generator CJS should add env configuration 1`] = `
"const { FlatCompat } = require('@eslint/eslintrc');
const js = require('@eslint/js');
const nxEslintPlugin = require('@nx/eslint-plugin');
@ -61,7 +61,7 @@ module.exports = [
"
`;
exports[`convert-to-flat-config generator should add global and env configuration 1`] = `
exports[`convert-to-flat-config generator CJS should add global and env configuration 1`] = `
"const { FlatCompat } = require('@eslint/eslintrc');
const js = require('@eslint/js');
const nxEslintPlugin = require('@nx/eslint-plugin');
@ -126,7 +126,7 @@ module.exports = [
"
`;
exports[`convert-to-flat-config generator should add global configuration 1`] = `
exports[`convert-to-flat-config generator CJS should add global configuration 1`] = `
"const { FlatCompat } = require('@eslint/eslintrc');
const js = require('@eslint/js');
const nxEslintPlugin = require('@nx/eslint-plugin');
@ -186,7 +186,7 @@ module.exports = [
"
`;
exports[`convert-to-flat-config generator should add global eslintignores 1`] = `
exports[`convert-to-flat-config generator CJS should add global eslintignores 1`] = `
"const { FlatCompat } = require('@eslint/eslintrc');
const js = require('@eslint/js');
const nxEslintPlugin = require('@nx/eslint-plugin');
@ -248,7 +248,7 @@ module.exports = [
"
`;
exports[`convert-to-flat-config generator should add parser 1`] = `
exports[`convert-to-flat-config generator CJS should add parser 1`] = `
"const { FlatCompat } = require('@eslint/eslintrc');
const js = require('@eslint/js');
const nxEslintPlugin = require('@nx/eslint-plugin');
@ -309,7 +309,7 @@ module.exports = [
"
`;
exports[`convert-to-flat-config generator should add plugins 1`] = `
exports[`convert-to-flat-config generator CJS should add plugins 1`] = `
"const { FlatCompat } = require('@eslint/eslintrc');
const js = require('@eslint/js');
const eslintPluginImport = require('eslint-plugin-import');
@ -378,7 +378,7 @@ module.exports = [
"
`;
exports[`convert-to-flat-config generator should add settings 1`] = `
exports[`convert-to-flat-config generator CJS should add settings 1`] = `
"const { FlatCompat } = require('@eslint/eslintrc');
const js = require('@eslint/js');
const nxEslintPlugin = require('@nx/eslint-plugin');
@ -442,7 +442,7 @@ module.exports = [
"
`;
exports[`convert-to-flat-config generator should convert json successfully 1`] = `
exports[`convert-to-flat-config generator CJS should convert json successfully 1`] = `
"const { FlatCompat } = require('@eslint/eslintrc');
const js = require('@eslint/js');
const nxEslintPlugin = require('@nx/eslint-plugin');
@ -501,7 +501,7 @@ module.exports = [
"
`;
exports[`convert-to-flat-config generator should convert json successfully 2`] = `
exports[`convert-to-flat-config generator CJS should convert json successfully 2`] = `
"const baseConfig = require('../../eslint.config.cjs');
module.exports = [
@ -528,7 +528,7 @@ module.exports = [
"
`;
exports[`convert-to-flat-config generator should convert yaml successfully 1`] = `
exports[`convert-to-flat-config generator CJS should convert yaml successfully 1`] = `
"const { FlatCompat } = require('@eslint/eslintrc');
const js = require('@eslint/js');
const nxEslintPlugin = require('@nx/eslint-plugin');
@ -587,7 +587,7 @@ module.exports = [
"
`;
exports[`convert-to-flat-config generator should convert yaml successfully 2`] = `
exports[`convert-to-flat-config generator CJS should convert yaml successfully 2`] = `
"const baseConfig = require('../../eslint.config.cjs');
module.exports = [
@ -614,7 +614,7 @@ module.exports = [
"
`;
exports[`convert-to-flat-config generator should convert yml successfully 1`] = `
exports[`convert-to-flat-config generator CJS should convert yml successfully 1`] = `
"const { FlatCompat } = require('@eslint/eslintrc');
const js = require('@eslint/js');
const nxEslintPlugin = require('@nx/eslint-plugin');
@ -673,7 +673,7 @@ module.exports = [
"
`;
exports[`convert-to-flat-config generator should convert yml successfully 2`] = `
exports[`convert-to-flat-config generator CJS should convert yml successfully 2`] = `
"const baseConfig = require('../../eslint.config.cjs');
module.exports = [
@ -700,7 +700,7 @@ module.exports = [
"
`;
exports[`convert-to-flat-config generator should handle custom eslintignores 1`] = `
exports[`convert-to-flat-config generator CJS should handle custom eslintignores 1`] = `
"const baseConfig = require('../../eslint.config.cjs');
module.exports = [
@ -732,3 +732,756 @@ module.exports = [
];
"
`;
exports[`convert-to-flat-config generator MJS should add env configuration 1`] = `
"import { FlatCompat } from '@eslint/eslintrc';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import js from '@eslint/js';
import nxEslintPlugin from '@nx/eslint-plugin';
import globals from 'globals';
const compat = new FlatCompat({
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
export default [
{
ignores: ['**/dist'],
},
{ plugins: { '@nx': nxEslintPlugin } },
{ languageOptions: { globals: { ...globals.browser, ...globals.node } } },
{
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', '**/*.cts', '**/*.mts'],
rules: {
...config.rules,
},
})),
...compat
.config({
extends: ['plugin:@nx/javascript'],
})
.map((config) => ({
...config,
files: ['**/*.js', '**/*.jsx', '**/*.cjs', '**/*.mjs'],
rules: {
...config.rules,
},
})),
];
"
`;
exports[`convert-to-flat-config generator MJS should add global and env configuration 1`] = `
"import { FlatCompat } from '@eslint/eslintrc';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import js from '@eslint/js';
import nxEslintPlugin from '@nx/eslint-plugin';
import globals from 'globals';
const compat = new FlatCompat({
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
export default [
{
ignores: ['**/dist'],
},
{ plugins: { '@nx': nxEslintPlugin } },
{
languageOptions: {
globals: { ...globals.browser, myCustomGlobal: 'readonly' },
},
},
{
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', '**/*.cts', '**/*.mts'],
rules: {
...config.rules,
},
})),
...compat
.config({
extends: ['plugin:@nx/javascript'],
})
.map((config) => ({
...config,
files: ['**/*.js', '**/*.jsx', '**/*.cjs', '**/*.mjs'],
rules: {
...config.rules,
},
})),
];
"
`;
exports[`convert-to-flat-config generator MJS should add global configuration 1`] = `
"import { FlatCompat } from '@eslint/eslintrc';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import js from '@eslint/js';
import nxEslintPlugin from '@nx/eslint-plugin';
const compat = new FlatCompat({
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
export default [
{
ignores: ['**/dist'],
},
{ plugins: { '@nx': nxEslintPlugin } },
{ languageOptions: { globals: { myCustomGlobal: 'readonly' } } },
{
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', '**/*.cts', '**/*.mts'],
rules: {
...config.rules,
},
})),
...compat
.config({
extends: ['plugin:@nx/javascript'],
})
.map((config) => ({
...config,
files: ['**/*.js', '**/*.jsx', '**/*.cjs', '**/*.mjs'],
rules: {
...config.rules,
},
})),
];
"
`;
exports[`convert-to-flat-config generator MJS should add global eslintignores 1`] = `
"import { FlatCompat } from '@eslint/eslintrc';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import js from '@eslint/js';
import nxEslintPlugin from '@nx/eslint-plugin';
const compat = new FlatCompat({
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
export default [
{
ignores: ['**/dist'],
},
{ 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', '**/*.cts', '**/*.mts'],
rules: {
...config.rules,
},
})),
...compat
.config({
extends: ['plugin:@nx/javascript'],
})
.map((config) => ({
...config,
files: ['**/*.js', '**/*.jsx', '**/*.cjs', '**/*.mjs'],
rules: {
...config.rules,
},
})),
{
ignores: ['ignore/me'],
},
];
"
`;
exports[`convert-to-flat-config generator MJS should add parser 1`] = `
"import { FlatCompat } from '@eslint/eslintrc';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import js from '@eslint/js';
import nxEslintPlugin from '@nx/eslint-plugin';
import typescriptEslintParser from '@typescript-eslint/parser';
const compat = new FlatCompat({
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
export default [
{
ignores: ['**/dist'],
},
{ plugins: { '@nx': nxEslintPlugin } },
{ languageOptions: { parser: typescriptEslintParser } },
{
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', '**/*.cts', '**/*.mts'],
rules: {
...config.rules,
},
})),
...compat
.config({
extends: ['plugin:@nx/javascript'],
})
.map((config) => ({
...config,
files: ['**/*.js', '**/*.jsx', '**/*.cjs', '**/*.mjs'],
rules: {
...config.rules,
},
})),
];
"
`;
exports[`convert-to-flat-config generator MJS should add plugins 1`] = `
"import { FlatCompat } from '@eslint/eslintrc';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import js from '@eslint/js';
import eslintPluginImport from 'eslint-plugin-import';
import eslintPluginSingleName from 'eslint-plugin-single-name';
import scopeEslintPluginWithName from '@scope/eslint-plugin-with-name';
import justScopeEslintPlugin from '@just-scope/eslint-plugin';
const compat = new FlatCompat({
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
export default [
{
ignores: ['**/dist'],
},
{
plugins: {
'eslint-plugin-import': eslintPluginImport,
'single-name': eslintPluginSingleName,
'@scope/with-name': scopeEslintPluginWithName,
'@just-scope': justScopeEslintPlugin,
},
},
{
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', '**/*.cts', '**/*.mts'],
rules: {
...config.rules,
},
})),
...compat
.config({
extends: ['plugin:@nx/javascript'],
})
.map((config) => ({
...config,
files: ['**/*.js', '**/*.jsx', '**/*.cjs', '**/*.mjs'],
rules: {
...config.rules,
},
})),
];
"
`;
exports[`convert-to-flat-config generator MJS should add settings 1`] = `
"import { FlatCompat } from '@eslint/eslintrc';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import js from '@eslint/js';
import nxEslintPlugin from '@nx/eslint-plugin';
const compat = new FlatCompat({
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
export default [
{
ignores: ['**/dist'],
},
{ plugins: { '@nx': nxEslintPlugin } },
{
settings: {
sharedData: 'Hello',
},
},
{
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', '**/*.cts', '**/*.mts'],
rules: {
...config.rules,
},
})),
...compat
.config({
extends: ['plugin:@nx/javascript'],
})
.map((config) => ({
...config,
files: ['**/*.js', '**/*.jsx', '**/*.cjs', '**/*.mjs'],
rules: {
...config.rules,
},
})),
];
"
`;
exports[`convert-to-flat-config generator MJS should convert json successfully 1`] = `
"import { FlatCompat } from '@eslint/eslintrc';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import js from '@eslint/js';
import nxEslintPlugin from '@nx/eslint-plugin';
const compat = new FlatCompat({
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
export default [
{
ignores: ['**/dist'],
},
{ 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', '**/*.cts', '**/*.mts'],
rules: {
...config.rules,
},
})),
...compat
.config({
extends: ['plugin:@nx/javascript'],
})
.map((config) => ({
...config,
files: ['**/*.js', '**/*.jsx', '**/*.cjs', '**/*.mjs'],
rules: {
...config.rules,
},
})),
];
"
`;
exports[`convert-to-flat-config generator MJS should convert json successfully 2`] = `
"import baseConfig from '../../eslint.config.mjs';
export default [
{
ignores: ['**/dist'],
},
...baseConfig,
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {},
},
{
files: ['**/*.ts', '**/*.tsx'],
// Override or add rules here
rules: {},
},
{
files: ['**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {},
},
];
"
`;
exports[`convert-to-flat-config generator MJS should convert yaml successfully 1`] = `
"import { FlatCompat } from '@eslint/eslintrc';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import js from '@eslint/js';
import nxEslintPlugin from '@nx/eslint-plugin';
const compat = new FlatCompat({
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
export default [
{
ignores: ['**/dist'],
},
{ 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', '**/*.cts', '**/*.mts'],
rules: {
...config.rules,
},
})),
...compat
.config({
extends: ['plugin:@nx/javascript'],
})
.map((config) => ({
...config,
files: ['**/*.js', '**/*.jsx', '**/*.cjs', '**/*.mjs'],
rules: {
...config.rules,
},
})),
];
"
`;
exports[`convert-to-flat-config generator MJS should convert yaml successfully 2`] = `
"import baseConfig from '../../eslint.config.mjs';
export default [
{
ignores: ['**/dist'],
},
...baseConfig,
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {},
},
{
files: ['**/*.ts', '**/*.tsx'],
// Override or add rules here
rules: {},
},
{
files: ['**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {},
},
];
"
`;
exports[`convert-to-flat-config generator MJS should convert yml successfully 1`] = `
"import { FlatCompat } from '@eslint/eslintrc';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import js from '@eslint/js';
import nxEslintPlugin from '@nx/eslint-plugin';
const compat = new FlatCompat({
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
export default [
{
ignores: ['**/dist'],
},
{ 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', '**/*.cts', '**/*.mts'],
rules: {
...config.rules,
},
})),
...compat
.config({
extends: ['plugin:@nx/javascript'],
})
.map((config) => ({
...config,
files: ['**/*.js', '**/*.jsx', '**/*.cjs', '**/*.mjs'],
rules: {
...config.rules,
},
})),
];
"
`;
exports[`convert-to-flat-config generator MJS should convert yml successfully 2`] = `
"import baseConfig from '../../eslint.config.mjs';
export default [
{
ignores: ['**/dist'],
},
...baseConfig,
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {},
},
{
files: ['**/*.ts', '**/*.tsx'],
// Override or add rules here
rules: {},
},
{
files: ['**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {},
},
];
"
`;
exports[`convert-to-flat-config generator MJS should handle custom eslintignores 1`] = `
"import baseConfig from '../../eslint.config.mjs';
export default [
{
ignores: ['**/dist'],
},
...baseConfig,
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {},
},
{
files: ['**/*.ts', '**/*.tsx'],
// Override or add rules here
rules: {},
},
{
files: ['**/*.js', '**/*.jsx'],
// Override or add rules here
rules: {},
},
{
ignores: ['ignore/me'],
},
{
ignores: ['ignore/me/as/well'],
},
];
"
`;

View File

@ -9,306 +9,623 @@ describe('convertEslintJsonToFlatConfig', () => {
tree = createTreeWithEmptyWorkspace();
});
it('should convert root configs', async () => {
tree.write(
'.eslintrc.json',
JSON.stringify({
root: true,
ignorePatterns: ['**/*', 'src/ignore/to/keep.ts'],
plugins: ['@nx'],
overrides: [
{
files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
rules: {
'@nx/enforce-module-boundaries': [
'error',
{
enforceBuildableLibDependency: true,
allow: [],
depConstraints: [
{
sourceTag: '*',
onlyDependOnLibsWithTags: ['*'],
},
],
},
],
},
},
{
files: ['*.ts', '*.tsx'],
extends: ['plugin:@nx/typescript'],
rules: {},
},
{
files: ['*.js', '*.jsx'],
extends: ['plugin:@nx/javascript'],
rules: {},
},
{
files: [
'**/*.spec.ts',
'**/*.spec.tsx',
'**/*.spec.js',
'**/*.spec.jsx',
],
env: {
jest: true,
},
rules: {},
},
],
})
);
tree.write('.eslintignore', 'node_modules\nsomething/else');
const { content } = convertEslintJsonToFlatConfig(
tree,
'',
readJson(tree, '.eslintrc.json'),
['.eslintignore']
);
expect(content).toMatchInlineSnapshot(`
"const { FlatCompat } = require("@eslint/eslintrc");
const js = require("@eslint/js");
const nxEslintPlugin = require("@nx/eslint-plugin");
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
});
module.exports = [
{
ignores: [
"**/dist"
]
},
{ plugins: { "@nx": nxEslintPlugin } },
{
files: [
"**/*.ts",
"**/*.tsx",
"**/*.js",
"**/*.jsx"
],
describe('ESM', () => {
it('should convert root configs', async () => {
tree.write(
'.eslintrc.json',
JSON.stringify({
root: true,
ignorePatterns: ['**/*', 'src/ignore/to/keep.ts'],
plugins: ['@nx'],
overrides: [
{
files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
rules: {
"@nx/enforce-module-boundaries": [
"error",
'@nx/enforce-module-boundaries': [
'error',
{
enforceBuildableLibDependency: true,
allow: [],
depConstraints: [
{
enforceBuildableLibDependency: true,
allow: [],
depConstraints: [
{
sourceTag: "*",
onlyDependOnLibsWithTags: [
"*"
]
}
sourceTag: '*',
onlyDependOnLibsWithTags: ['*'],
},
],
},
],
},
},
{
files: ['*.ts', '*.tsx'],
extends: ['plugin:@nx/typescript'],
rules: {},
},
{
files: ['*.js', '*.jsx'],
extends: ['plugin:@nx/javascript'],
rules: {},
},
{
files: [
'**/*.spec.ts',
'**/*.spec.tsx',
'**/*.spec.js',
'**/*.spec.jsx',
],
env: {
jest: true,
},
rules: {},
},
],
})
);
tree.write('.eslintignore', 'node_modules\nsomething/else');
const { content } = convertEslintJsonToFlatConfig(
tree,
'',
readJson(tree, '.eslintrc.json'),
['.eslintignore'],
'mjs'
);
expect(content).toMatchInlineSnapshot(`
"import { FlatCompat } from "@eslint/eslintrc";
import { dirname } from "path";
import { fileURLToPath } from "url";
import js from "@eslint/js";
import nxEslintPlugin from "@nx/eslint-plugin";
const compat = new FlatCompat({
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
export default [
{
ignores: [
"**/dist"
]
},
{ 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",
"**/*.cts",
"**/*.mts"
],
rules: {
...config.rules
}
})),
...compat.config({
extends: [
"plugin:@nx/javascript"
]
}).map(config => ({
...config,
files: [
"**/*.js",
"**/*.jsx",
"**/*.cjs",
"**/*.mjs"
],
rules: {
...config.rules
}
})),
...compat.config({
env: {
jest: true
}
}).map(config => ({
...config,
files: [
"**/*.spec.ts",
"**/*.spec.tsx",
"**/*.spec.js",
"**/*.spec.jsx"
],
rules: {
...config.rules
}
})),
{
ignores: [
"src/ignore/to/keep.ts"
]
},
{
ignores: [
"something/else"
]
}
];
"
`);
});
it('should convert project configs', async () => {
tree.write(
'mylib/.eslintrc.json',
JSON.stringify({
extends: [
'plugin:@nx/react-typescript',
'next',
'next/core-web-vitals',
'../../.eslintrc.json',
],
ignorePatterns: ['!**/*', '.next/**/*'],
overrides: [
{
files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
rules: {
'@next/next/no-html-link-for-pages': [
'error',
'apps/test-next/pages',
],
},
},
{
files: ['*.ts', '*.tsx'],
rules: {},
},
{
files: ['*.js', '*.jsx'],
rules: {},
},
{
files: ['*.json'],
parser: 'jsonc-eslint-parser',
rules: {
'@nx/dependency-checks': 'error',
},
},
],
rules: {
'@next/next/no-html-link-for-pages': 'off',
},
env: {
jest: true,
},
})
);
tree.write('mylib/.eslintignore', 'node_modules\nsomething/else');
const { content } = convertEslintJsonToFlatConfig(
tree,
'mylib',
readJson(tree, 'mylib/.eslintrc.json'),
['mylib/.eslintignore'],
'mjs'
);
expect(content).toMatchInlineSnapshot(`
"import { FlatCompat } from "@eslint/eslintrc";
import { dirname } from "path";
import { fileURLToPath } from "url";
import js from "@eslint/js";
import baseConfig from "../../eslint.config.mjs";
import globals from "globals";
const compat = new FlatCompat({
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
export default [
{
ignores: [
"**/dist"
]
},
...baseConfig,
...compat.extends("plugin:@nx/react-typescript", "next", "next/core-web-vitals"),
{ languageOptions: { globals: { ...globals.jest } } },
{
rules: {
"@next/next/no-html-link-for-pages": "off"
}
},
{
files: [
"**/*.ts",
"**/*.tsx",
"**/*.js",
"**/*.jsx"
],
rules: {
"@next/next/no-html-link-for-pages": [
"error",
"apps/test-next/pages"
]
}
]
}
},
...compat.config({
extends: [
"plugin:@nx/typescript"
]
}).map(config => ({
...config,
files: [
"**/*.ts",
"**/*.tsx",
"**/*.cts",
"**/*.mts"
],
rules: {
...config.rules
}
})),
...compat.config({
extends: [
"plugin:@nx/javascript"
]
}).map(config => ({
...config,
files: [
"**/*.js",
"**/*.jsx",
"**/*.cjs",
"**/*.mjs"
],
rules: {
...config.rules
}
})),
...compat.config({
env: {
jest: true
}
}).map(config => ({
...config,
files: [
"**/*.spec.ts",
"**/*.spec.tsx",
"**/*.spec.js",
"**/*.spec.jsx"
],
rules: {
...config.rules
}
})),
{
ignores: [
"src/ignore/to/keep.ts"
]
},
{
ignores: [
"something/else"
]
}
];
"
`);
},
{
files: [
"**/*.ts",
"**/*.tsx"
],
// Override or add rules here
rules: {}
},
{
files: [
"**/*.js",
"**/*.jsx"
],
// Override or add rules here
rules: {}
},
{
files: [
"**/*.json"
],
rules: {
"@nx/dependency-checks": "error"
},
languageOptions: {
parser: await import("jsonc-eslint-parser")
}
},
{
ignores: [
".next/**/*"
]
},
{
ignores: [
"something/else"
]
}
];
"
`);
});
});
it('should convert project configs', async () => {
tree.write(
'mylib/.eslintrc.json',
JSON.stringify({
extends: [
'plugin:@nx/react-typescript',
'next',
'next/core-web-vitals',
'../../.eslintrc.json',
],
ignorePatterns: ['!**/*', '.next/**/*'],
overrides: [
{
files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
rules: {
'@next/next/no-html-link-for-pages': [
'error',
'apps/test-next/pages',
],
},
},
{
files: ['*.ts', '*.tsx'],
rules: {},
},
{
files: ['*.js', '*.jsx'],
rules: {},
},
{
files: ['*.json'],
parser: 'jsonc-eslint-parser',
rules: {
'@nx/dependency-checks': 'error',
},
},
],
rules: {
'@next/next/no-html-link-for-pages': 'off',
},
env: {
jest: true,
},
})
);
tree.write('mylib/.eslintignore', 'node_modules\nsomething/else');
const { content } = convertEslintJsonToFlatConfig(
tree,
'mylib',
readJson(tree, 'mylib/.eslintrc.json'),
['mylib/.eslintignore']
);
expect(content).toMatchInlineSnapshot(`
"const { FlatCompat } = require("@eslint/eslintrc");
const js = require("@eslint/js");
const baseConfig = require("../../eslint.config.cjs");
const globals = require("globals");
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
});
module.exports = [
{
ignores: [
"**/dist"
]
},
...baseConfig,
...compat.extends("plugin:@nx/react-typescript", "next", "next/core-web-vitals"),
{ languageOptions: { globals: { ...globals.jest } } },
{
describe('CJS', () => {
it('should convert root configs', async () => {
tree.write(
'.eslintrc.json',
JSON.stringify({
root: true,
ignorePatterns: ['**/*', 'src/ignore/to/keep.ts'],
plugins: ['@nx'],
overrides: [
{
files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
rules: {
"@next/next/no-html-link-for-pages": "off"
}
},
{
files: [
"**/*.ts",
"**/*.tsx",
"**/*.js",
"**/*.jsx"
],
rules: {
"@next/next/no-html-link-for-pages": [
"error",
"apps/test-next/pages"
]
}
},
{
files: [
"**/*.ts",
"**/*.tsx"
],
// Override or add rules here
rules: {}
},
{
files: [
"**/*.js",
"**/*.jsx"
],
// Override or add rules here
rules: {}
},
{
files: [
"**/*.json"
],
rules: {
"@nx/dependency-checks": "error"
'@nx/enforce-module-boundaries': [
'error',
{
enforceBuildableLibDependency: true,
allow: [],
depConstraints: [
{
sourceTag: '*',
onlyDependOnLibsWithTags: ['*'],
},
],
},
],
},
languageOptions: {
parser: require("jsonc-eslint-parser")
}
},
{
files: ['*.ts', '*.tsx'],
extends: ['plugin:@nx/typescript'],
rules: {},
},
{
files: ['*.js', '*.jsx'],
extends: ['plugin:@nx/javascript'],
rules: {},
},
{
files: [
'**/*.spec.ts',
'**/*.spec.tsx',
'**/*.spec.js',
'**/*.spec.jsx',
],
env: {
jest: true,
},
rules: {},
},
],
})
);
tree.write('.eslintignore', 'node_modules\nsomething/else');
const { content } = convertEslintJsonToFlatConfig(
tree,
'',
readJson(tree, '.eslintrc.json'),
['.eslintignore'],
'cjs'
);
expect(content).toMatchInlineSnapshot(`
"const { FlatCompat } = require("@eslint/eslintrc");
const js = require("@eslint/js");
const nxEslintPlugin = require("@nx/eslint-plugin");
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
});
module.exports = [
{
ignores: [
"**/dist"
]
},
{ 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",
"**/*.cts",
"**/*.mts"
],
rules: {
...config.rules
}
})),
...compat.config({
extends: [
"plugin:@nx/javascript"
]
}).map(config => ({
...config,
files: [
"**/*.js",
"**/*.jsx",
"**/*.cjs",
"**/*.mjs"
],
rules: {
...config.rules
}
})),
...compat.config({
env: {
jest: true
}
}).map(config => ({
...config,
files: [
"**/*.spec.ts",
"**/*.spec.tsx",
"**/*.spec.js",
"**/*.spec.jsx"
],
rules: {
...config.rules
}
})),
{
ignores: [
"src/ignore/to/keep.ts"
]
},
{
ignores: [
"something/else"
]
}
];
"
`);
});
it('should convert project configs', async () => {
tree.write(
'mylib/.eslintrc.json',
JSON.stringify({
extends: [
'plugin:@nx/react-typescript',
'next',
'next/core-web-vitals',
'../../.eslintrc.json',
],
ignorePatterns: ['!**/*', '.next/**/*'],
overrides: [
{
files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
rules: {
'@next/next/no-html-link-for-pages': [
'error',
'apps/test-next/pages',
],
},
},
{
files: ['*.ts', '*.tsx'],
rules: {},
},
{
files: ['*.js', '*.jsx'],
rules: {},
},
{
files: ['*.json'],
parser: 'jsonc-eslint-parser',
rules: {
'@nx/dependency-checks': 'error',
},
},
],
rules: {
'@next/next/no-html-link-for-pages': 'off',
},
{
ignores: [
".next/**/*"
]
env: {
jest: true,
},
{
ignores: [
"something/else"
]
}
];
"
`);
})
);
tree.write('mylib/.eslintignore', 'node_modules\nsomething/else');
const { content } = convertEslintJsonToFlatConfig(
tree,
'mylib',
readJson(tree, 'mylib/.eslintrc.json'),
['mylib/.eslintignore'],
'cjs'
);
expect(content).toMatchInlineSnapshot(`
"const { FlatCompat } = require("@eslint/eslintrc");
const js = require("@eslint/js");
const baseConfig = require("../../eslint.config.cjs");
const globals = require("globals");
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
});
module.exports = [
{
ignores: [
"**/dist"
]
},
...baseConfig,
...compat.extends("plugin:@nx/react-typescript", "next", "next/core-web-vitals"),
{ languageOptions: { globals: { ...globals.jest } } },
{
rules: {
"@next/next/no-html-link-for-pages": "off"
}
},
{
files: [
"**/*.ts",
"**/*.tsx",
"**/*.js",
"**/*.jsx"
],
rules: {
"@next/next/no-html-link-for-pages": [
"error",
"apps/test-next/pages"
]
}
},
{
files: [
"**/*.ts",
"**/*.tsx"
],
// Override or add rules here
rules: {}
},
{
files: [
"**/*.js",
"**/*.jsx"
],
// Override or add rules here
rules: {}
},
{
files: [
"**/*.json"
],
rules: {
"@nx/dependency-checks": "error"
},
languageOptions: {
parser: require("jsonc-eslint-parser")
}
},
{
ignores: [
".next/**/*"
]
},
{
ignores: [
"something/else"
]
}
];
"
`);
});
});
});

View File

@ -21,7 +21,8 @@ export function convertEslintJsonToFlatConfig(
tree: Tree,
root: string,
config: ESLint.ConfigData,
ignorePaths: string[]
ignorePaths: string[],
format: 'cjs' | 'mjs'
): { content: string; addESLintRC: boolean; addESLintJS: boolean } {
const importsMap = new Map<string, string>();
const exportElements: ts.Expression[] = [];
@ -38,7 +39,12 @@ export function convertEslintJsonToFlatConfig(
);
if (config.extends) {
const extendsResult = addExtends(importsMap, exportElements, config);
const extendsResult = addExtends(
importsMap,
exportElements,
config,
format
);
isFlatCompatNeeded = extendsResult.isFlatCompatNeeded;
isESLintJSNeeded = extendsResult.isESLintJSNeeded;
}
@ -156,7 +162,7 @@ export function convertEslintJsonToFlatConfig(
) {
isFlatCompatNeeded = true;
}
exportElements.push(generateFlatOverride(override));
exportElements.push(generateFlatOverride(override, format));
});
}
@ -189,7 +195,7 @@ export function convertEslintJsonToFlatConfig(
}
// create the node list and print it to new file
const nodeList = createNodeList(importsMap, exportElements);
const nodeList = createNodeList(importsMap, exportElements, format);
let content = stringifyNodeList(nodeList);
if (isFlatCompatNeeded) {
content = addFlatCompatToFlatConfig(content);
@ -206,7 +212,8 @@ export function convertEslintJsonToFlatConfig(
function addExtends(
importsMap: Map<string, string | string[]>,
configBlocks: ts.Expression[],
config: ESLint.ConfigData
config: ESLint.ConfigData,
format: 'mjs' | 'cjs'
): { isFlatCompatNeeded: boolean; isESLintJSNeeded: boolean } {
let isFlatCompatNeeded = false;
let isESLintJSNeeded = false;
@ -225,7 +232,7 @@ function addExtends(
configBlocks.push(generateSpreadElement(localName));
const newImport = imp.replace(
/^(.*)\.eslintrc(.base)?\.json$/,
'$1eslint$2.config.cjs'
`$1eslint$2.config.${format}`
);
importsMap.set(newImport, localName);
} else {

View File

@ -40,10 +40,12 @@ export async function convertToFlatConfigGenerator(
);
}
options.eslintConfigFormat ??= 'mjs';
const eslintIgnoreFiles = new Set<string>(['.eslintignore']);
// convert root eslint config to eslint.config.cjs
convertRootToFlatConfig(tree, eslintFile);
// convert root eslint config to eslint.config.cjs or eslint.base.config.mjs based on eslintConfigFormat
convertRootToFlatConfig(tree, eslintFile, options.eslintConfigFormat);
// convert project eslint files to eslint.config.cjs
const projects = getProjects(tree);
@ -53,7 +55,8 @@ export async function convertToFlatConfigGenerator(
project,
projectConfig,
readNxJson(tree),
eslintIgnoreFiles
eslintIgnoreFiles,
options.eslintConfigFormat
);
}
@ -63,7 +66,7 @@ export async function convertToFlatConfigGenerator(
}
// replace references in nx.json
updateNxJsonConfig(tree);
updateNxJsonConfig(tree, options.eslintConfigFormat);
// install missing packages
if (!options.skipFormat) {
@ -75,15 +78,26 @@ export async function convertToFlatConfigGenerator(
export default convertToFlatConfigGenerator;
function convertRootToFlatConfig(tree: Tree, eslintFile: string) {
function convertRootToFlatConfig(
tree: Tree,
eslintFile: string,
format: 'cjs' | 'mjs'
) {
if (/\.base\.(js|json|yml|yaml)$/.test(eslintFile)) {
convertConfigToFlatConfig(tree, '', eslintFile, 'eslint.base.config.cjs');
convertConfigToFlatConfig(
tree,
'',
eslintFile,
`eslint.base.config.${format}`,
format
);
}
convertConfigToFlatConfig(
tree,
'',
eslintFile.replace('.base.', '.'),
'eslint.config.cjs'
`eslint.config.${format}`,
format
);
}
@ -92,7 +106,8 @@ function convertProjectToFlatConfig(
project: string,
projectConfig: ProjectConfiguration,
nxJson: NxJsonConfiguration,
eslintIgnoreFiles: Set<string>
eslintIgnoreFiles: Set<string>,
format: 'cjs' | 'mjs'
) {
const eslintFile = findEslintFile(tree, projectConfig.root);
if (eslintFile && !eslintFile.endsWith('.js')) {
@ -132,7 +147,8 @@ function convertProjectToFlatConfig(
tree,
projectConfig.root,
eslintFile,
'eslint.config.cjs',
`eslint.config.${format}`,
format,
ignorePath
);
eslintIgnoreFiles.add(`${projectConfig.root}/.eslintignore`);
@ -146,22 +162,22 @@ function convertProjectToFlatConfig(
// update names of eslint files in nx.json
// and remove eslintignore
function updateNxJsonConfig(tree: Tree) {
function updateNxJsonConfig(tree: Tree, format: 'cjs' | 'mjs') {
if (tree.exists('nx.json')) {
updateJson(tree, 'nx.json', (json: NxJsonConfiguration) => {
if (json.targetDefaults?.lint?.inputs) {
const inputSet = new Set(json.targetDefaults.lint.inputs);
inputSet.add('{workspaceRoot}/eslint.config.cjs');
inputSet.add(`{workspaceRoot}/eslint.config.${format}`);
json.targetDefaults.lint.inputs = Array.from(inputSet);
}
if (json.targetDefaults?.['@nx/eslint:lint']?.inputs) {
const inputSet = new Set(json.targetDefaults['@nx/eslint:lint'].inputs);
inputSet.add('{workspaceRoot}/eslint.config.cjs');
inputSet.add(`{workspaceRoot}/eslint.config.${format}`);
json.targetDefaults['@nx/eslint:lint'].inputs = Array.from(inputSet);
}
if (json.namedInputs?.production) {
const inputSet = new Set(json.namedInputs.production);
inputSet.add('!{projectRoot}/eslint.config.cjs');
inputSet.add(`!{projectRoot}/eslint.config.${format}`);
json.namedInputs.production = Array.from(inputSet);
}
return json;
@ -174,6 +190,7 @@ function convertConfigToFlatConfig(
root: string,
source: string,
target: string,
format: 'cjs' | 'mjs',
ignorePath?: string
) {
const ignorePaths = ignorePath
@ -186,7 +203,8 @@ function convertConfigToFlatConfig(
tree,
root,
config,
ignorePaths
ignorePaths,
format
);
return processConvertedConfig(tree, root, source, target, conversionResult);
}
@ -201,7 +219,8 @@ function convertConfigToFlatConfig(
tree,
root,
config,
ignorePaths
ignorePaths,
format
);
return processConvertedConfig(tree, root, source, target, conversionResult);
}

View File

@ -1,3 +1,5 @@
export interface ConvertToFlatConfigGeneratorSchema {
skipFormat?: boolean;
// Internal option
eslintConfigFormat?: 'mjs' | 'cjs';
}

View File

@ -66,6 +66,7 @@ function postTargetTransformer(
'{workspaceRoot}/.eslintrc.json',
'{workspaceRoot}/.eslintignore',
'{workspaceRoot}/eslint.config.cjs',
'{workspaceRoot}/eslint.config.mjs',
].includes(input)
);
if (inputs.length === 0) {

View File

@ -92,9 +92,10 @@ export const getGlobalEsLintConfiguration = (
};
export const getGlobalFlatEslintConfiguration = (
format: 'cjs' | 'mjs',
rootProject?: boolean
): string => {
const nodeList = createNodeList(new Map(), []);
const nodeList = createNodeList(new Map(), [], format);
let content = stringifyNodeList(nodeList);
content = addImportToFlatConfig(content, 'nx', '@nx/eslint-plugin');
@ -114,49 +115,58 @@ export const getGlobalFlatEslintConfiguration = (
content = addBlockToFlatConfigExport(
content,
generateFlatOverride({
ignores: ['**/dist'],
})
generateFlatOverride(
{
ignores: ['**/dist'],
},
format
)
);
if (!rootProject) {
content = addBlockToFlatConfigExport(
content,
generateFlatOverride({
files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
rules: {
'@nx/enforce-module-boundaries': [
'error',
{
enforceBuildableLibDependency: true,
allow: [
// This allows a root project to be present without causing lint errors
// since all projects will depend on this base file.
'^.*/eslint(\\.base)?\\.config\\.[cm]?js$',
],
depConstraints: [
{ sourceTag: '*', onlyDependOnLibsWithTags: ['*'] },
],
},
],
} as Linter.RulesRecord,
})
generateFlatOverride(
{
files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
rules: {
'@nx/enforce-module-boundaries': [
'error',
{
enforceBuildableLibDependency: true,
allow: [
// This allows a root project to be present without causing lint errors
// since all projects will depend on this base file.
'^.*/eslint(\\.base)?\\.config\\.[cm]?js$',
],
depConstraints: [
{ sourceTag: '*', onlyDependOnLibsWithTags: ['*'] },
],
},
],
} as Linter.RulesRecord,
},
format
)
);
}
content = addBlockToFlatConfigExport(
content,
generateFlatOverride({
files: [
'**/*.ts',
'**/*.tsx',
'**/*.js',
'**/*.jsx',
'**/*.cjs',
'**/*.mjs',
],
rules: {},
})
generateFlatOverride(
{
files: [
'**/*.ts',
'**/*.tsx',
'**/*.js',
'**/*.jsx',
'**/*.cjs',
'**/*.mjs',
],
rules: {},
},
format
)
);
return content;

View File

@ -9,8 +9,12 @@ import {
updateJson,
writeJson,
} from '@nx/devkit';
import { dirname } from 'path';
import { findEslintFile, isEslintConfigSupported } from '../utils/eslint-file';
import { dirname, extname } from 'path';
import {
determineEslintConfigFormat,
findEslintFile,
isEslintConfigSupported,
} from '../utils/eslint-file';
import {
getGlobalEsLintConfiguration,
getGlobalFlatEslintConfiguration,
@ -32,10 +36,24 @@ export function migrateConfigToMonorepoStyle(
projects: ProjectConfiguration[],
tree: Tree,
unitTestRunner: string,
eslintConfigFormat: 'mjs' | 'cjs',
keepExistingVersions?: boolean
): GeneratorCallback {
const rootEslintConfig = findEslintFile(tree);
let skipCleanup = false;
if (rootEslintConfig) {
// We do not want to mix the formats
const fileExtension = extname(rootEslintConfig);
if (fileExtension === '.mjs' || fileExtension === '.cjs') {
eslintConfigFormat = fileExtension.slice(1) as 'mjs' | 'cjs';
} else {
eslintConfigFormat = determineEslintConfigFormat(
tree.read(rootEslintConfig, 'utf-8')
);
}
}
if (
rootEslintConfig?.match(/\.base\./) &&
!projects.some((p) => p.root === '.')
@ -57,10 +75,10 @@ export function migrateConfigToMonorepoStyle(
keepExistingVersions
);
tree.write(
tree.exists('eslint.config.cjs')
? 'eslint.base.config.cjs'
: 'eslint.config.cjs',
getGlobalFlatEslintConfiguration()
tree.exists(`eslint.config.${eslintConfigFormat}`)
? `eslint.base.config.${eslintConfigFormat}`
: `eslint.config.${eslintConfigFormat}`,
getGlobalFlatEslintConfiguration(eslintConfigFormat)
);
} else {
const eslintFile = findEslintFile(tree, '.');
@ -134,7 +152,9 @@ function migrateEslintFile(projectEslintPath: string, tree: Tree) {
let config = tree.read(projectEslintPath, 'utf-8');
// remove @nx plugin
config = removePlugin(config, '@nx', '@nx/eslint-plugin-nx');
// extend eslint.base.config.cjs
// if base config is cjs, we will need to import it using async import
config = addImportToFlatConfig(
config,
'baseConfig',

View File

@ -102,43 +102,103 @@ describe('@nx/eslint:init', () => {
});
describe('(legacy)', () => {
it('should add the root eslint config to the lint targetDefaults for lint', async () => {
await lintInitGenerator(tree, { ...options, addPlugin: false });
describe('CJS', () => {
it('should add the root eslint config to the lint targetDefaults for lint', async () => {
await lintInitGenerator(tree, {
...options,
addPlugin: false,
eslintConfigFormat: 'cjs',
});
expect(
readJson(tree, 'nx.json').targetDefaults['@nx/eslint:lint']
).toEqual({
cache: true,
inputs: [
'default',
'{workspaceRoot}/.eslintrc.json',
'{workspaceRoot}/.eslintignore',
'{workspaceRoot}/eslint.config.cjs',
],
expect(
readJson(tree, 'nx.json').targetDefaults['@nx/eslint:lint']
).toEqual({
cache: true,
inputs: [
'default',
'{workspaceRoot}/.eslintrc.json',
'{workspaceRoot}/.eslintignore',
'{workspaceRoot}/eslint.config.cjs',
],
});
});
it('should setup lint target defaults', async () => {
updateJson<NxJsonConfiguration>(tree, 'nx.json', (json) => {
json.namedInputs ??= {};
json.namedInputs.production = ['default'];
return json;
});
await lintInitGenerator(tree, {
...options,
addPlugin: false,
eslintConfigFormat: 'cjs',
});
expect(
readJson<NxJsonConfiguration>(tree, 'nx.json').targetDefaults[
'@nx/eslint:lint'
]
).toEqual({
cache: true,
inputs: [
'default',
'{workspaceRoot}/.eslintrc.json',
'{workspaceRoot}/.eslintignore',
'{workspaceRoot}/eslint.config.cjs',
],
});
});
});
it('should setup lint target defaults', async () => {
updateJson<NxJsonConfiguration>(tree, 'nx.json', (json) => {
json.namedInputs ??= {};
json.namedInputs.production = ['default'];
return json;
describe('MJS', () => {
it('should add the root eslint config to the lint targetDefaults for lint', async () => {
await lintInitGenerator(tree, {
...options,
addPlugin: false,
eslintConfigFormat: 'mjs',
});
expect(
readJson(tree, 'nx.json').targetDefaults['@nx/eslint:lint']
).toEqual({
cache: true,
inputs: [
'default',
'{workspaceRoot}/.eslintrc.json',
'{workspaceRoot}/.eslintignore',
'{workspaceRoot}/eslint.config.mjs',
],
});
});
await lintInitGenerator(tree, { ...options, addPlugin: false });
it('should setup lint target defaults', async () => {
updateJson<NxJsonConfiguration>(tree, 'nx.json', (json) => {
json.namedInputs ??= {};
json.namedInputs.production = ['default'];
return json;
});
expect(
readJson<NxJsonConfiguration>(tree, 'nx.json').targetDefaults[
'@nx/eslint:lint'
]
).toEqual({
cache: true,
inputs: [
'default',
'{workspaceRoot}/.eslintrc.json',
'{workspaceRoot}/.eslintignore',
'{workspaceRoot}/eslint.config.cjs',
],
await lintInitGenerator(tree, {
...options,
addPlugin: false,
eslintConfigFormat: 'mjs',
});
expect(
readJson<NxJsonConfiguration>(tree, 'nx.json').targetDefaults[
'@nx/eslint:lint'
]
).toEqual({
cache: true,
inputs: [
'default',
'{workspaceRoot}/.eslintrc.json',
'{workspaceRoot}/.eslintignore',
'{workspaceRoot}/eslint.config.mjs',
],
});
});
});
});

View File

@ -11,31 +11,37 @@ import {
} from '@nx/devkit';
import { addPlugin } from '@nx/devkit/src/utils/add-plugin';
import { eslintVersion, nxVersion } from '../../utils/versions';
import { findEslintFile } from '../utils/eslint-file';
import {
determineEslintConfigFormat,
findEslintFile,
} from '../utils/eslint-file';
import { createNodesV2 } from '../../plugins/plugin';
import { hasEslintPlugin } from '../utils/plugin';
import { extname } from 'path';
export interface LinterInitOptions {
skipPackageJson?: boolean;
keepExistingVersions?: boolean;
updatePackageScripts?: boolean;
addPlugin?: boolean;
// Internal option
eslintConfigFormat?: 'mjs' | 'cjs';
}
function updateProductionFileset(tree: Tree) {
function updateProductionFileset(tree: Tree, format: 'mjs' | 'cjs' = 'mjs') {
const nxJson = readNxJson(tree);
const productionFileSet = nxJson.namedInputs?.production;
if (productionFileSet) {
productionFileSet.push('!{projectRoot}/.eslintrc.json');
productionFileSet.push('!{projectRoot}/eslint.config.cjs');
productionFileSet.push(`!{projectRoot}/eslint.config.${format}`);
// Dedupe and set
nxJson.namedInputs.production = Array.from(new Set(productionFileSet));
}
updateNxJson(tree, nxJson);
}
function addTargetDefaults(tree: Tree) {
function addTargetDefaults(tree: Tree, format: 'mjs' | 'cjs') {
const nxJson = readNxJson(tree);
nxJson.targetDefaults ??= {};
@ -45,7 +51,7 @@ function addTargetDefaults(tree: Tree) {
'default',
`{workspaceRoot}/.eslintrc.json`,
`{workspaceRoot}/.eslintignore`,
`{workspaceRoot}/eslint.config.cjs`,
`{workspaceRoot}/eslint.config.${format}`,
];
updateNxJson(tree, nxJson);
}
@ -74,9 +80,21 @@ export async function initEsLint(
process.env.NX_ADD_PLUGINS !== 'false' &&
nxJson.useInferencePlugins !== false;
options.addPlugin ??= addPluginDefault;
options.eslintConfigFormat ??= 'mjs';
const hasPlugin = hasEslintPlugin(tree);
const rootEslintFile = findEslintFile(tree);
if (rootEslintFile) {
const fileExtension = extname(rootEslintFile);
if (fileExtension === '.mjs' || fileExtension === '.cjs') {
options.eslintConfigFormat = fileExtension.slice(1) as 'mjs' | 'cjs';
} else {
options.eslintConfigFormat = determineEslintConfigFormat(
tree.read(rootEslintFile, 'utf-8')
);
}
}
const graph = await createProjectGraphAsync();
const lintTargetNames = [
@ -107,7 +125,7 @@ export async function initEsLint(
return () => {};
}
updateProductionFileset(tree);
updateProductionFileset(tree, options.eslintConfigFormat);
updateVsCodeRecommendedExtensions(tree);
@ -123,7 +141,7 @@ export async function initEsLint(
options.updatePackageScripts
);
} else {
addTargetDefaults(tree);
addTargetDefaults(tree, options.eslintConfigFormat);
}
const tasks: GeneratorCallback[] = [];

View File

@ -42,7 +42,237 @@ describe('@nx/eslint:lint-project', () => {
});
});
it('should generate a flat eslint base config', async () => {
describe('Eslint base config named eslint.base.config', () => {
it('should generate a flat eslint config format based on base config (JS with CJS export)', async () => {
const originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG;
process.env.ESLINT_USE_FLAT_CONFIG = 'true';
// CJS config
tree.write('eslint.base.config.js', 'module.exports = {};');
await lintProjectGenerator(tree, {
...defaultOptions,
linter: Linter.EsLint,
project: 'test-lib',
setParserOptionsProject: false,
skipFormat: true,
});
expect(tree.read('libs/test-lib/eslint.config.cjs', 'utf-8'))
.toMatchInlineSnapshot(`
"const baseConfig = require("../../eslint.base.config.js");
module.exports = [
...baseConfig
];
"
`);
process.env.ESLINT_USE_FLAT_CONFIG = originalEslintUseFlatConfigVal;
});
it('should generate a flat eslint config format based on base config (JS with MJS export)', async () => {
const originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG;
process.env.ESLINT_USE_FLAT_CONFIG = 'true';
// MJS config
tree.write('eslint.base.config.js', 'export default {};');
await lintProjectGenerator(tree, {
...defaultOptions,
linter: Linter.EsLint,
project: 'test-lib',
setParserOptionsProject: false,
skipFormat: true,
});
expect(tree.read('libs/test-lib/eslint.config.mjs', 'utf-8'))
.toMatchInlineSnapshot(`
"import baseConfig from "../../eslint.base.config.js";
export default [
...baseConfig
];
"
`);
process.env.ESLINT_USE_FLAT_CONFIG = originalEslintUseFlatConfigVal;
});
it('should generate a flat eslint config format based on base config (mjs)', async () => {
const originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG;
process.env.ESLINT_USE_FLAT_CONFIG = 'true';
// MJS config
tree.write('eslint.base.config.mjs', 'export default {};');
await lintProjectGenerator(tree, {
...defaultOptions,
linter: Linter.EsLint,
project: 'test-lib',
setParserOptionsProject: false,
skipFormat: true,
eslintConfigFormat: 'mjs',
});
expect(tree.read('libs/test-lib/eslint.config.mjs', 'utf-8'))
.toMatchInlineSnapshot(`
"import baseConfig from "../../eslint.base.config.mjs";
export default [
...baseConfig
];
"
`);
process.env.ESLINT_USE_FLAT_CONFIG = originalEslintUseFlatConfigVal;
});
it('should generate a flat eslint config format based on base config CJS', async () => {
const originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG;
process.env.ESLINT_USE_FLAT_CONFIG = 'true';
// CJS config
tree.write('eslint.base.config.cjs', 'module.exports = {};');
await lintProjectGenerator(tree, {
...defaultOptions,
linter: Linter.EsLint,
project: 'test-lib',
setParserOptionsProject: false,
skipFormat: true,
});
expect(tree.read('libs/test-lib/eslint.config.cjs', 'utf-8'))
.toMatchInlineSnapshot(`
"const baseConfig = require("../../eslint.base.config.cjs");
module.exports = [
...baseConfig
];
"
`);
process.env.ESLINT_USE_FLAT_CONFIG = originalEslintUseFlatConfigVal;
});
});
describe('Eslint base config named eslint.config', () => {
it('should generate a flat eslint config format based on base config (JS with CJS export)', async () => {
const originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG;
process.env.ESLINT_USE_FLAT_CONFIG = 'true';
// CJS config
tree.write('eslint.config.js', 'module.exports = {};');
await lintProjectGenerator(tree, {
...defaultOptions,
linter: Linter.EsLint,
project: 'test-lib',
setParserOptionsProject: false,
skipFormat: true,
});
expect(tree.read('libs/test-lib/eslint.config.cjs', 'utf-8'))
.toMatchInlineSnapshot(`
"const baseConfig = require("../../eslint.config.js");
module.exports = [
...baseConfig
];
"
`);
process.env.ESLINT_USE_FLAT_CONFIG = originalEslintUseFlatConfigVal;
});
it('should generate a flat eslint config format based on base config (JS with MJS export)', async () => {
const originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG;
process.env.ESLINT_USE_FLAT_CONFIG = 'true';
// MJS config
tree.write('eslint.config.js', 'export default {};');
await lintProjectGenerator(tree, {
...defaultOptions,
linter: Linter.EsLint,
project: 'test-lib',
setParserOptionsProject: false,
skipFormat: true,
});
expect(tree.read('libs/test-lib/eslint.config.mjs', 'utf-8'))
.toMatchInlineSnapshot(`
"import baseConfig from "../../eslint.config.js";
export default [
...baseConfig
];
"
`);
process.env.ESLINT_USE_FLAT_CONFIG = originalEslintUseFlatConfigVal;
});
it('should generate a flat eslint config format based on base config (mjs)', async () => {
const originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG;
process.env.ESLINT_USE_FLAT_CONFIG = 'true';
// MJS config
tree.write('eslint.config.mjs', 'export default {};');
await lintProjectGenerator(tree, {
...defaultOptions,
linter: Linter.EsLint,
project: 'test-lib',
setParserOptionsProject: false,
skipFormat: true,
eslintConfigFormat: 'mjs',
});
expect(tree.read('libs/test-lib/eslint.config.mjs', 'utf-8'))
.toMatchInlineSnapshot(`
"import baseConfig from "../../eslint.config.mjs";
export default [
...baseConfig
];
"
`);
process.env.ESLINT_USE_FLAT_CONFIG = originalEslintUseFlatConfigVal;
});
it('should generate a flat eslint config format based on base config CJS', async () => {
const originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG;
process.env.ESLINT_USE_FLAT_CONFIG = 'true';
// CJS config
tree.write('eslint.config.cjs', 'module.exports = {};');
await lintProjectGenerator(tree, {
...defaultOptions,
linter: Linter.EsLint,
project: 'test-lib',
setParserOptionsProject: false,
skipFormat: true,
});
expect(tree.read('libs/test-lib/eslint.config.cjs', 'utf-8'))
.toMatchInlineSnapshot(`
"const baseConfig = require("../../eslint.config.cjs");
module.exports = [
...baseConfig
];
"
`);
process.env.ESLINT_USE_FLAT_CONFIG = originalEslintUseFlatConfigVal;
});
});
it('should generate a flat eslint base config ESM', async () => {
const originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG;
process.env.ESLINT_USE_FLAT_CONFIG = 'true';
await lintProjectGenerator(tree, {
@ -51,6 +281,76 @@ describe('@nx/eslint:lint-project', () => {
project: 'test-lib',
setParserOptionsProject: false,
skipFormat: true,
eslintConfigFormat: 'mjs',
});
expect(tree.read('eslint.config.mjs', 'utf-8')).toMatchInlineSnapshot(`
"import nx from "@nx/eslint-plugin";
export default [
...nx.configs["flat/base"],
...nx.configs["flat/typescript"],
...nx.configs["flat/javascript"],
{
ignores: [
"**/dist"
]
},
{
files: [
"**/*.ts",
"**/*.tsx",
"**/*.js",
"**/*.jsx"
],
rules: {
"@nx/enforce-module-boundaries": [
"error",
{
enforceBuildableLibDependency: true,
allow: [
"^.*/eslint(\\\\.base)?\\\\.config\\\\.[cm]?js$"
],
depConstraints: [
{
sourceTag: "*",
onlyDependOnLibsWithTags: [
"*"
]
}
]
}
]
}
},
{
files: [
"**/*.ts",
"**/*.tsx",
"**/*.js",
"**/*.jsx",
"**/*.cjs",
"**/*.mjs"
],
// Override or add rules here
rules: {}
}
];
"
`);
process.env.ESLINT_USE_FLAT_CONFIG = originalEslintUseFlatConfigVal;
});
it('should generate a flat eslint base config CJS', async () => {
const originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG;
process.env.ESLINT_USE_FLAT_CONFIG = 'true';
await lintProjectGenerator(tree, {
...defaultOptions,
linter: Linter.EsLint,
project: 'test-lib',
setParserOptionsProject: false,
skipFormat: true,
eslintConfigFormat: 'cjs',
});
expect(tree.read('eslint.config.cjs', 'utf-8')).toMatchInlineSnapshot(`

View File

@ -17,8 +17,11 @@ import {
} from '@nx/devkit';
import { Linter as LinterEnum, LinterType } from '../utils/linter';
import { findEslintFile } from '../utils/eslint-file';
import { join } from 'path';
import {
determineEslintConfigFormat,
findEslintFile,
} from '../utils/eslint-file';
import { extname, join } from 'path';
import { lintInitGenerator } from '../init/init';
import type { Linter } from 'eslint';
import { migrateConfigToMonorepoStyle } from '../init/init-migration';
@ -32,7 +35,7 @@ import {
} from '../utils/flat-config/ast-utils';
import {
baseEsLintConfigFile,
baseEsLintFlatConfigFile,
BASE_ESLINT_CONFIG_FILENAMES,
} from '../../utils/config-file';
import { hasEslintPlugin } from '../utils/plugin';
import { setupRootEsLint } from './setup-root-eslint';
@ -49,6 +52,7 @@ interface LintProjectOptions {
rootProject?: boolean;
keepExistingVersions?: boolean;
addPlugin?: boolean;
eslintConfigFormat?: 'mjs' | 'cjs';
/**
* @internal
@ -66,6 +70,7 @@ export async function lintProjectGeneratorInternal(
options: LintProjectOptions
) {
const nxJson = readNxJson(tree);
options.eslintConfigFormat ??= 'mjs';
const addPluginDefault =
process.env.NX_ADD_PLUGINS !== 'false' &&
nxJson.useInferencePlugins !== false;
@ -74,12 +79,14 @@ export async function lintProjectGeneratorInternal(
const initTask = await lintInitGenerator(tree, {
skipPackageJson: options.skipPackageJson,
addPlugin: options.addPlugin,
eslintConfigFormat: options.eslintConfigFormat,
});
tasks.push(initTask);
const rootEsLintTask = setupRootEsLint(tree, {
unitTestRunner: options.unitTestRunner,
skipPackageJson: options.skipPackageJson,
rootProject: options.rootProject,
eslintConfigFormat: options.eslintConfigFormat,
});
tasks.push(rootEsLintTask);
const projectConfig = readProjectConfiguration(tree, options.project);
@ -146,6 +153,7 @@ export async function lintProjectGeneratorInternal(
filteredProjects,
tree,
options.unitTestRunner,
options.eslintConfigFormat,
options.keepExistingVersions
);
tasks.push(migrateTask);
@ -199,6 +207,22 @@ function createEsLintConfiguration(
const pathToRootConfig = extendedRootConfig
? `${offsetFromRoot(projectConfig.root)}${extendedRootConfig}`
: undefined;
if (extendedRootConfig) {
// We do not want to mix the formats
// if the base file extension is `.mjs` we should use `mjs` for the new file
// or if base the file extension is `.cjs` then the format should be `cjs`
const fileExtension = extname(extendedRootConfig);
if (fileExtension === '.mjs' || fileExtension === '.cjs') {
options.eslintConfigFormat = fileExtension.slice(1) as 'mjs' | 'cjs';
} else {
options.eslintConfigFormat = determineEslintConfigFormat(
tree.read(extendedRootConfig, 'utf-8')
);
}
}
const addDependencyChecks =
options.addPackageJsonDependencyChecks ||
isBuildableLibraryProject(projectConfig);
@ -269,11 +293,18 @@ function createEsLintConfiguration(
nodes.push(generateSpreadElement('baseConfig'));
}
overrides.forEach((override) => {
nodes.push(generateFlatOverride(override));
nodes.push(generateFlatOverride(override, options.eslintConfigFormat));
});
const nodeList = createNodeList(importMap, nodes);
const nodeList = createNodeList(
importMap,
nodes,
options.eslintConfigFormat
);
const content = stringifyNodeList(nodeList);
tree.write(join(projectConfig.root, `eslint.config.cjs`), content);
tree.write(
join(projectConfig.root, `eslint.config.${options.eslintConfigFormat}`),
content
);
} else {
writeJson(tree, join(projectConfig.root, `.eslintrc.json`), {
extends: extendedRootConfig ? [pathToRootConfig] : undefined,
@ -313,8 +344,9 @@ function isBuildableLibraryProject(
function isMigrationToMonorepoNeeded(tree: Tree, graph: ProjectGraph): boolean {
// the base config is already created, migration has been done
if (
tree.exists(baseEsLintConfigFile) ||
tree.exists(baseEsLintFlatConfigFile)
[baseEsLintConfigFile, ...BASE_ESLINT_CONFIG_FILENAMES].some((f) =>
tree.exists(f)
)
) {
return false;
}

View File

@ -22,6 +22,7 @@ export type SetupRootEsLintOptions = {
unitTestRunner?: string;
skipPackageJson?: boolean;
rootProject?: boolean;
eslintConfigFormat?: 'mjs' | 'cjs';
};
export function setupRootEsLint(
@ -32,6 +33,8 @@ export function setupRootEsLint(
if (rootEslintFile) {
return () => {};
}
options.eslintConfigFormat ??= 'mjs';
if (!useFlatConfig(tree)) {
return setUpLegacyRootEslintRc(tree, options);
}
@ -71,8 +74,11 @@ function setUpLegacyRootEslintRc(tree: Tree, options: SetupRootEsLintOptions) {
function setUpRootFlatConfig(tree: Tree, options: SetupRootEsLintOptions) {
tree.write(
'eslint.config.cjs',
getGlobalFlatEslintConfiguration(options.rootProject)
`eslint.config.${options.eslintConfigFormat}`,
getGlobalFlatEslintConfiguration(
options.eslintConfigFormat,
options.rootProject
)
);
return !options.skipPackageJson

View File

@ -2,8 +2,8 @@ import { readJson, type Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import * as devkitInternals from 'nx/src/devkit-internals';
import {
BASE_ESLINT_CONFIG_FILENAMES,
ESLINT_CONFIG_FILENAMES,
baseEsLintConfigFile,
} from '../../utils/config-file';
import {
addExtendsToLintConfig,
@ -32,12 +32,11 @@ describe('@nx/eslint:lint-file', () => {
}
);
test.each(ESLINT_CONFIG_FILENAMES)(
'should return base file instead %p when calling findEslintFile',
test.each(BASE_ESLINT_CONFIG_FILENAMES)(
'should return base file %p when calling findEslintFile',
(eslintFileName) => {
tree.write(baseEsLintConfigFile, '{}');
tree.write(eslintFileName, '{}');
expect(findEslintFile(tree)).toBe(baseEsLintConfigFile);
expect(findEslintFile(tree)).toBe(eslintFileName);
}
);
});

View File

@ -12,9 +12,8 @@ import type { Linter } from 'eslint';
import { gte } from 'semver';
import {
baseEsLintConfigFile,
baseEsLintFlatConfigFile,
ESLINT_CONFIG_FILENAMES,
legacyBaseEsLintFlatConfigFile,
BASE_ESLINT_CONFIG_FILENAMES,
} from '../../utils/config-file';
import {
eslintFlatConfigFilenames,
@ -45,17 +44,15 @@ export function findEslintFile(
tree: Tree,
projectRoot?: string
): string | null {
if (projectRoot === undefined && tree.exists(baseEsLintConfigFile)) {
return baseEsLintConfigFile;
}
if (projectRoot === undefined && tree.exists(baseEsLintFlatConfigFile)) {
return baseEsLintFlatConfigFile;
}
if (
projectRoot === undefined &&
tree.exists(legacyBaseEsLintFlatConfigFile)
) {
return legacyBaseEsLintFlatConfigFile;
if (projectRoot === undefined) {
for (const file of [
baseEsLintConfigFile,
...BASE_ESLINT_CONFIG_FILENAMES,
]) {
if (tree.exists(file)) {
return file;
}
}
}
projectRoot ??= '';
for (const file of ESLINT_CONFIG_FILENAMES) {
@ -75,7 +72,8 @@ export function isEslintConfigSupported(tree: Tree, projectRoot = ''): boolean {
return (
eslintFile.endsWith('.json') ||
eslintFile.endsWith('.config.js') ||
eslintFile.endsWith('.config.cjs')
eslintFile.endsWith('.config.cjs') ||
eslintFile.endsWith('.config.mjs')
);
}
@ -148,6 +146,19 @@ function replaceFlatConfigPaths(
`require('${newPath}')` +
newConfig.slice(match.index + match[0].length);
}
// Handle import statements
const importRegex = RegExp(/import\s+.*?\s+from\s+['"](.*)['"]/g);
while ((match = importRegex.exec(newConfig)) !== null) {
const oldPath = match[1];
const newPath = offsetFilePath(sourceRoot, oldPath, offset, tree);
// Replace the old path with the updated path
newConfig =
newConfig.slice(0, match.index + match[0].indexOf(oldPath)) +
newPath +
newConfig.slice(match.index + match[0].indexOf(oldPath) + oldPath.length);
}
// replace projects
const projectRegex = RegExp(/project:\s?\[?['"](.*)['"]\]?/g);
while ((match = projectRegex.exec(newConfig)) !== null) {
@ -184,6 +195,22 @@ function offsetFilePath(
return joinPathFragments(offset, projectRoot, pathToFile);
}
export function determineEslintConfigFormat(content: string): 'mjs' | 'cjs' {
const sourceFile = ts.createSourceFile(
'',
content,
ts.ScriptTarget.Latest,
true
);
// Check if there's an `export default` in the AST
const hasExportDefault = sourceFile.statements.some(
(statement) => ts.isExportAssignment(statement) && !statement.isExportEquals
);
return hasExportDefault ? 'mjs' : 'cjs';
}
export function addOverrideToLintConfig(
tree: Tree,
root: string,
@ -197,7 +224,12 @@ export function addOverrideToLintConfig(
if (useFlatConfig(tree)) {
let fileName: string;
if (isBase) {
fileName = joinPathFragments(root, baseEsLintFlatConfigFile);
for (const file of BASE_ESLINT_CONFIG_FILENAMES) {
if (tree.exists(joinPathFragments(root, file))) {
fileName = joinPathFragments(root, file);
break;
}
}
} else {
for (const f of eslintFlatConfigFilenames) {
if (tree.exists(joinPathFragments(root, f))) {
@ -207,8 +239,10 @@ export function addOverrideToLintConfig(
}
}
const flatOverride = generateFlatOverride(override);
let content = tree.read(fileName, 'utf8');
const format = content.includes('export default') ? 'mjs' : 'cjs';
const flatOverride = generateFlatOverride(override, format);
// Check if the provided override using legacy eslintrc properties or plugins, if so we need to add compat
if (overrideNeedsCompat(override)) {
content = addFlatCompatToFlatConfig(content);
@ -306,7 +340,12 @@ export function lintConfigHasOverride(
checkBaseConfig &&
findEslintFile(tree, root).includes('.base');
if (isBase) {
fileName = joinPathFragments(root, baseEsLintFlatConfigFile);
for (const file of BASE_ESLINT_CONFIG_FILENAMES) {
if (tree.exists(joinPathFragments(root, file))) {
fileName = joinPathFragments(root, file);
break;
}
}
}
if (useFlatConfig(tree)) {
if (!fileName) {
@ -343,13 +382,14 @@ export function replaceOverridesInLintConfig(
}
}
let content = tree.read(fileName, 'utf8');
const format = content.includes('export default') ? 'mjs' : 'cjs';
// Check if any of the provided overrides using legacy eslintrc properties or plugins, if so we need to add compat
if (overrides.some(overrideNeedsCompat)) {
content = addFlatCompatToFlatConfig(content);
}
content = removeOverridesFromLintConfig(content);
overrides.forEach((override) => {
const flatOverride = generateFlatOverride(override);
const flatOverride = generateFlatOverride(override, format);
content = addBlockToFlatConfigExport(content, flatOverride);
});
@ -381,6 +421,14 @@ export function addExtendsToLintConfig(
break;
}
}
// Check the file extension to determine the format of the config if it is .js we look for the export
const eslintConfigFormat = fileName.endsWith('.mjs')
? 'mjs'
: fileName.endsWith('.cjs')
? 'cjs'
: tree.read(fileName, 'utf-8').includes('module.exports')
? 'cjs'
: 'mjs';
let shouldImportEslintCompat = false;
// assume eslint version is 9 if not found, as it's what we'd be generating by default

View File

@ -30,7 +30,7 @@ describe('ast-utils', () => {
it('should create appropriate ASTs for a flat config entries based on the provided legacy eslintrc JSON override data', () => {
// It's easier to review the stringified result of the AST than the AST itself
const getOutput = (input: any) => {
const ast = generateFlatOverride(input);
const ast = generateFlatOverride(input, 'mjs');
return printTsNode(ast);
};
@ -87,13 +87,13 @@ describe('ast-utils', () => {
expect(
getOutput({
// It should not only nest the parser in languageOptions, but also wrap it in a require call because parsers are passed by reference in flat config
// It should not only nest the parser in languageOptions, but also wrap it in an import call because parsers are passed by reference in flat config
parser: 'jsonc-eslint-parser',
})
).toMatchInlineSnapshot(`
"{
languageOptions: {
parser: require("jsonc-eslint-parser")
parser: await import("jsonc-eslint-parser")
}
}"
`);
@ -188,8 +188,8 @@ describe('ast-utils', () => {
describe('addBlockToFlatConfigExport', () => {
it('should inject block to the end of the file', () => {
const content = `const baseConfig = require("../../eslint.config.cjs");
module.exports = [
const content = `import baseConfig from "../../eslint.config.mjs";
export default [
...baseConfig,
{
files: [
@ -210,17 +210,18 @@ describe('ast-utils', () => {
})
);
expect(result).toMatchInlineSnapshot(`
"const baseConfig = require("../../eslint.config.cjs");
module.exports = [
...baseConfig,
{
files: [
"my-lib/**/*.ts",
"my-lib/**/*.tsx"
],
rules: {}
},
{ ignores: ["my-lib/.cache/**/*"] },
"import baseConfig from "../../eslint.config.mjs";
export default [
...baseConfig,
{
files: [
"my-lib/**/*.ts",
"my-lib/**/*.tsx"
],
rules: {}
},
{ ignores: ["my-lib/.cache/**/*"] },
{
files: [
"**/*.svg"
@ -228,14 +229,15 @@ describe('ast-utils', () => {
rules: {
"@nx/do-something-with-svg": "error"
}
},
];"
}
];
"
`);
});
it('should inject spread to the beginning of the file', () => {
const content = `const baseConfig = require("../../eslint.config.cjs");
module.exports = [
const content = `import baseConfig from "../../eslint.config.mjs";
export default [
...baseConfig,
{
files: [
@ -252,28 +254,29 @@ describe('ast-utils', () => {
{ insertAtTheEnd: false }
);
expect(result).toMatchInlineSnapshot(`
"const baseConfig = require("../../eslint.config.cjs");
module.exports = [
...config,
"import baseConfig from "../../eslint.config.mjs";
...baseConfig,
{
files: [
"my-lib/**/*.ts",
"my-lib/**/*.tsx"
],
rules: {}
},
{ ignores: ["my-lib/.cache/**/*"] },
];"
export default [
...config,
...baseConfig,
{
files: [
"my-lib/**/*.ts",
"my-lib/**/*.tsx"
],
rules: {}
},
{ ignores: ["my-lib/.cache/**/*"] }
];
"
`);
});
});
describe('addImportToFlatConfig', () => {
it('should inject import if not found', () => {
const content = `const baseConfig = require("../../eslint.config.cjs");
module.exports = [
const content = `import baseConfig from "../../eslint.config.mjs";
export default [
...baseConfig,
{
files: [
@ -286,30 +289,30 @@ describe('ast-utils', () => {
];`;
const result = addImportToFlatConfig(
content,
'varName',
['varName'],
'@myorg/awesome-config'
);
expect(result).toMatchInlineSnapshot(`
"const varName = require("@myorg/awesome-config");
const baseConfig = require("../../eslint.config.cjs");
module.exports = [
...baseConfig,
{
files: [
"my-lib/**/*.ts",
"my-lib/**/*.tsx"
],
rules: {}
},
{ ignores: ["my-lib/.cache/**/*"] },
];"
`);
"import { varName } from "@myorg/awesome-config";
import baseConfig from "../../eslint.config.mjs";
export default [
...baseConfig,
{
files: [
"my-lib/**/*.ts",
"my-lib/**/*.tsx"
],
rules: {}
},
{ ignores: ["my-lib/.cache/**/*"] },
];"
`);
});
it('should update import if already found', () => {
const content = `const { varName } = require("@myorg/awesome-config");
const baseConfig = require("../../eslint.config.cjs");
module.exports = [
const content = `import { varName } from "@myorg/awesome-config";
import baseConfig from "../../eslint.config.mjs";
export default [
...baseConfig,
{
files: [
@ -326,26 +329,27 @@ describe('ast-utils', () => {
'@myorg/awesome-config'
);
expect(result).toMatchInlineSnapshot(`
"const { varName, otherName, someName } = require("@myorg/awesome-config");
const baseConfig = require("../../eslint.config.cjs");
module.exports = [
...baseConfig,
{
files: [
"my-lib/**/*.ts",
"my-lib/**/*.tsx"
],
rules: {}
},
{ ignores: ["my-lib/.cache/**/*"] },
];"
`);
"import { varName, otherName, someName } from "@myorg/awesome-config";
import baseConfig from "../../eslint.config.mjs";
export default [
...baseConfig,
{
files: [
"my-lib/**/*.ts",
"my-lib/**/*.tsx"
],
rules: {}
},
{ ignores: ["my-lib/.cache/**/*"] },
];"
`);
});
it('should not inject import if already exists', () => {
const content = `const { varName, otherName } = require("@myorg/awesome-config");
const baseConfig = require("../../eslint.config.cjs");
module.exports = [
const content = `import { varName, otherName } from "@myorg/awesome-config";
import baseConfig from "../../eslint.config.mjs";
export default [
...baseConfig,
{
files: [
@ -365,9 +369,10 @@ describe('ast-utils', () => {
});
it('should not update import if already exists', () => {
const content = `const varName = require("@myorg/awesome-config");
const baseConfig = require("../../eslint.config.cjs");
module.exports = [
const content = `import { varName } from "@myorg/awesome-config";
import baseConfig from "../../eslint.config.mjs";
export default [
...baseConfig,
{
files: [
@ -380,7 +385,7 @@ describe('ast-utils', () => {
];`;
const result = addImportToFlatConfig(
content,
'varName',
['varName'],
'@myorg/awesome-config'
);
expect(result).toEqual(content);
@ -390,10 +395,11 @@ describe('ast-utils', () => {
describe('removeImportFromFlatConfig', () => {
it('should remove existing import from config if the var name matches', () => {
const content = stripIndents`
const nx = require("@nx/eslint-plugin");
const thisShouldRemain = require("@nx/eslint-plugin");
const playwright = require('eslint-plugin-playwright');
module.exports = [
import nx from "@nx/eslint-plugin";
import thisShouldRemain from "@nx/eslint-plugin";
import playwright from 'eslint-plugin-playwright';
export default [
playwright.configs['flat/recommended'],
];
`;
@ -404,9 +410,10 @@ describe('ast-utils', () => {
);
expect(result).toMatchInlineSnapshot(`
"
const thisShouldRemain = require("@nx/eslint-plugin");
const playwright = require('eslint-plugin-playwright');
module.exports = [
import thisShouldRemain from "@nx/eslint-plugin";
import playwright from 'eslint-plugin-playwright';
export default [
playwright.configs['flat/recommended'],
];"
`);
@ -415,8 +422,8 @@ describe('ast-utils', () => {
describe('addCompatToFlatConfig', () => {
it('should add compat to config', () => {
const content = `const baseConfig = require("../../eslint.config.cjs");
module.exports = [
const content = `import baseConfig from "../../eslint.config.mjs";
export default [
...baseConfig,
{
files: [
@ -429,15 +436,18 @@ describe('ast-utils', () => {
];`;
const result = addFlatCompatToFlatConfig(content);
expect(result).toMatchInlineSnapshot(`
"const { FlatCompat } = require("@eslint/eslintrc");
const js = require("@eslint/js");
const baseConfig = require("../../eslint.config.cjs");
"import { FlatCompat } from "@eslint/eslintrc";
import { dirname } from "path";
import { fileURLToPath } from "url";
import js from "@eslint/js";
import baseConfig from "../../eslint.config.mjs";
const compat = new FlatCompat({
baseDirectory: __dirname,
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
module.exports = [
export default [
...baseConfig,
{
files: [
@ -452,9 +462,10 @@ describe('ast-utils', () => {
});
it('should add only partially compat to config if parts exist', () => {
const content = `const baseConfig = require("../../eslint.config.cjs");
const js = require("@eslint/js");
module.exports = [
const content = `import baseConfig from "../../eslint.config.mjs";
import js from "@eslint/js";
export default [
...baseConfig,
{
files: [
@ -467,15 +478,19 @@ describe('ast-utils', () => {
];`;
const result = addFlatCompatToFlatConfig(content);
expect(result).toMatchInlineSnapshot(`
"const { FlatCompat } = require("@eslint/eslintrc");
const baseConfig = require("../../eslint.config.cjs");
const js = require("@eslint/js");
"import { FlatCompat } from "@eslint/eslintrc";
import { dirname } from "path";
import { fileURLToPath } from "url";
import baseConfig from "../../eslint.config.mjs";
import js from "@eslint/js";
const compat = new FlatCompat({
baseDirectory: __dirname,
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
module.exports = [
export default [
...baseConfig,
{
files: [
@ -490,16 +505,18 @@ describe('ast-utils', () => {
});
it('should not add compat to config if exist', () => {
const content = `const FlatCompat = require("@eslint/eslintrc");
const baseConfig = require("../../eslint.config.cjs");
const js = require("@eslint/js");
const content = `import { FlatCompat } from "@eslint/eslintrc";
import baseConfig from "../../eslint.config.cjs";
import js from "@eslint/js";
import { fileURLToPath } from "url";
import { dirname } from 'path';
const compat = new FlatCompat({
baseDirectory: __dirname,
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
module.exports = [
export default [
...baseConfig,
{
files: [
@ -517,16 +534,17 @@ describe('ast-utils', () => {
describe('removeOverridesFromLintConfig', () => {
it('should remove all rules from config', () => {
const content = `const FlatCompat = require("@eslint/eslintrc");
const baseConfig = require("../../eslint.config.cjs");
const js = require("@eslint/js");
const content = `import { FlatCompat } from "@eslint/eslintrc";
import js from "@eslint/js";
import { fileURLToPath } from "url";
import { dirname } from 'path';
const compat = new FlatCompat({
baseDirectory: __dirname,
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
module.exports = [
export default [
...baseConfig,
{
files: [
@ -557,26 +575,27 @@ describe('ast-utils', () => {
];`;
const result = removeOverridesFromLintConfig(content);
expect(result).toMatchInlineSnapshot(`
"const FlatCompat = require("@eslint/eslintrc");
const baseConfig = require("../../eslint.config.cjs");
const js = require("@eslint/js");
"import { FlatCompat } from "@eslint/eslintrc";
import js from "@eslint/js";
import { fileURLToPath } from "url";
import { dirname } from 'path';
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
});
const compat = new FlatCompat({
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
module.exports = [
...baseConfig,
{ ignores: ["my-lib/.cache/**/*"] },
];"
`);
export default [
...baseConfig,
{ ignores: ["my-lib/.cache/**/*"] },
];"
`);
});
it('should remove all rules from starting with first', () => {
const content = `const baseConfig = require("../../eslint.config.cjs");
const content = `import baseConfig from "../../eslint.config.mjs";
module.exports = [
export default [
{
files: [
"my-lib/**/*.ts",
@ -605,19 +624,19 @@ describe('ast-utils', () => {
];`;
const result = removeOverridesFromLintConfig(content);
expect(result).toMatchInlineSnapshot(`
"const baseConfig = require("../../eslint.config.cjs");
"import baseConfig from "../../eslint.config.mjs";
module.exports = [
];"
`);
export default [
];"
`);
});
});
describe('replaceOverride', () => {
it('should find and replace rules in override', () => {
const content = `const baseConfig = require("../../eslint.config.cjs");
const content = `import baseConfig from "../../eslint.config.mjs";
module.exports = [
export default [
{
files: [
"my-lib/**/*.ts",
@ -657,9 +676,9 @@ module.exports = [
})
);
expect(result).toMatchInlineSnapshot(`
"const baseConfig = require("../../eslint.config.cjs");
"import baseConfig from "../../eslint.config.mjs";
module.exports = [
export default [
{
"files": [
"my-lib/**/*.ts",
@ -692,9 +711,9 @@ module.exports = [
});
it('should append rules in override', () => {
const content = `const baseConfig = require("../../eslint.config.cjs");
const content = `import baseConfig from "../../eslint.config.mjs";
module.exports = [
export default [
{
files: [
"my-lib/**/*.ts",
@ -728,9 +747,9 @@ module.exports = [
})
);
expect(result).toMatchInlineSnapshot(`
"const baseConfig = require("../../eslint.config.cjs");
"import baseConfig from "../../eslint.config.mjs";
module.exports = [
export default [
{
"files": [
"my-lib/**/*.ts",
@ -755,9 +774,9 @@ module.exports = [
});
it('should work for compat overrides', () => {
const content = `const baseConfig = require("../../eslint.config.cjs");
const content = `import baseConfig from "../../eslint.config.mjs";
module.exports = [
export default [
...compat.config({ extends: ["plugin:@nx/typescript"] }).map(config => ({
...config,
files: [
@ -783,9 +802,9 @@ module.exports = [
})
);
expect(result).toMatchInlineSnapshot(`
"const baseConfig = require("../../eslint.config.cjs");
"import baseConfig from "../../eslint.config.mjs";
module.exports = [
export default [
...compat.config({ extends: ["plugin:@nx/typescript"] }).map(config => ({
...config,
"files": [
@ -804,14 +823,17 @@ module.exports = [
describe('removePlugin', () => {
it('should remove plugins from config', () => {
const content = `const { FlatCompat } = require("@eslint/eslintrc");
const nxEslintPlugin = require("@nx/eslint-plugin");
const js = require("@eslint/js");
const content = `import { FlatCompat } from "@eslint/eslintrc";
import nxEslintPlugin from "@nx/eslint-plugin";
import js = from ("@eslint/js");
import { fileURLToPath} from "url";
import { dirname } from 'path';
const compat = new FlatCompat({
baseDirectory: __dirname,
baseDirectory: dirname(fileURLToPath(import.meta.url));
recommendedConfig: js.configs.recommended,
});
module.exports = [
export default [
{ plugins: { "@nx": nxEslintPlugin } },
{ ignores: ["src/ignore/to/keep.ts"] },
{ ignores: ["something/else"] }
@ -819,13 +841,16 @@ module.exports = [
const result = removePlugin(content, '@nx', '@nx/eslint-plugin');
expect(result).toMatchInlineSnapshot(`
"const { FlatCompat } = require("@eslint/eslintrc");
const js = require("@eslint/js");
"import { FlatCompat } from "@eslint/eslintrc";
import js = from ("@eslint/js");
import { fileURLToPath} from "url";
import { dirname } from 'path';
const compat = new FlatCompat({
baseDirectory: __dirname,
baseDirectory: dirname(fileURLToPath(import.meta.url));
recommendedConfig: js.configs.recommended,
});
module.exports = [
export default [
{ ignores: ["src/ignore/to/keep.ts"] },
{ ignores: ["something/else"] }
];"
@ -833,15 +858,19 @@ module.exports = [
});
it('should remove single plugin from config', () => {
const content = `const { FlatCompat } = require("@eslint/eslintrc");
const nxEslintPlugin = require("@nx/eslint-plugin");
const otherPlugin = require("other/eslint-plugin");
const js = require("@eslint/js");
const content = `import { FlatCompat } from "@eslint/eslintrc";
import nxEslintPlugin from "@nx/eslint-plugin";
import otherPlugin from "other/eslint-plugin";
import js from "@eslint/js";
import { fileURLToPath } from "url";
import { dirname } from 'path';
const compat = new FlatCompat({
baseDirectory: __dirname,
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
module.exports = [
export default [
{ plugins: { "@nx": nxEslintPlugin, "@other": otherPlugin } },
{ ignores: ["src/ignore/to/keep.ts"] },
{ ignores: ["something/else"] }
@ -849,14 +878,18 @@ module.exports = [
const result = removePlugin(content, '@nx', '@nx/eslint-plugin');
expect(result).toMatchInlineSnapshot(`
"const { FlatCompat } = require("@eslint/eslintrc");
const otherPlugin = require("other/eslint-plugin");
const js = require("@eslint/js");
"import { FlatCompat } from "@eslint/eslintrc";
import otherPlugin from "other/eslint-plugin";
import js from "@eslint/js";
import { fileURLToPath } from "url";
import { dirname } from 'path';
const compat = new FlatCompat({
baseDirectory: __dirname,
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
module.exports = [
export default [
{ plugins: { "@other": otherPlugin } },
{ ignores: ["src/ignore/to/keep.ts"] },
{ ignores: ["something/else"] }
@ -865,14 +898,18 @@ module.exports = [
});
it('should leave other properties in config', () => {
const content = `const { FlatCompat } = require("@eslint/eslintrc");
const nxEslintPlugin = require("@nx/eslint-plugin");
const js = require("@eslint/js");
const content = `import { FlatCompat } from "@eslint/eslintrc";
import nxEslintPlugin from "@nx/eslint-plugin";
import js from "@eslint/js";
import { fileURLToPath } from "url";
import { dirname } from 'path';
const compat = new FlatCompat({
baseDirectory: __dirname,
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
module.exports = [
export default [
{ plugins: { "@nx": nxEslintPlugin }, rules: {} },
{ ignores: ["src/ignore/to/keep.ts"] },
{ ignores: ["something/else"] }
@ -880,13 +917,17 @@ module.exports = [
const result = removePlugin(content, '@nx', '@nx/eslint-plugin');
expect(result).toMatchInlineSnapshot(`
"const { FlatCompat } = require("@eslint/eslintrc");
const js = require("@eslint/js");
"import { FlatCompat } from "@eslint/eslintrc";
import js from "@eslint/js";
import { fileURLToPath } from "url";
import { dirname } from 'path';
const compat = new FlatCompat({
baseDirectory: __dirname,
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
module.exports = [
export default [
{ rules: {} },
{ ignores: ["src/ignore/to/keep.ts"] },
{ ignores: ["something/else"] }
@ -895,14 +936,18 @@ module.exports = [
});
it('should remove single plugin from config array', () => {
const content = `const { FlatCompat } = require("@eslint/eslintrc");
const nxEslintPlugin = require("@nx/eslint-plugin");
const js = require("@eslint/js");
const content = `import { FlatCompat } from "@eslint/eslintrc";
import nxEslintPlugin from "@nx/eslint-plugin";
import js from "@eslint/js";
import { fileURLToPath } from "url";
import { dirname } from 'path';
const compat = new FlatCompat({
baseDirectory: __dirname,
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
module.exports = [
export default [
{ plugins: ["@nx", "something-else"] },
{ ignores: ["src/ignore/to/keep.ts"] },
{ ignores: ["something/else"] }
@ -910,13 +955,17 @@ module.exports = [
const result = removePlugin(content, '@nx', '@nx/eslint-plugin');
expect(result).toMatchInlineSnapshot(`
"const { FlatCompat } = require("@eslint/eslintrc");
const js = require("@eslint/js");
"import { FlatCompat } from "@eslint/eslintrc";
import js from "@eslint/js";
import { fileURLToPath } from "url";
import { dirname } from 'path';
const compat = new FlatCompat({
baseDirectory: __dirname,
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
module.exports = [
export default [
{ plugins:["something-else"] },
{ ignores: ["src/ignore/to/keep.ts"] },
{ ignores: ["something/else"] }
@ -925,14 +974,18 @@ module.exports = [
});
it('should leave other fields in the object', () => {
const content = `const { FlatCompat } = require("@eslint/eslintrc");
const nxEslintPlugin = require("@nx/eslint-plugin");
const js = require("@eslint/js");
const content = `import { FlatCompat } from "@eslint/eslintrc";
import nxEslintPlugin from "@nx/eslint-plugin";
import js from "@eslint/js";
import { fileURLToPath } from "url";
import { dirname } from 'path';
const compat = new FlatCompat({
baseDirectory: __dirname,
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
module.exports = [
export default [
{ plugins: ["@nx"], rules: { } },
{ ignores: ["src/ignore/to/keep.ts"] },
{ ignores: ["something/else"] }
@ -940,13 +993,17 @@ module.exports = [
const result = removePlugin(content, '@nx', '@nx/eslint-plugin');
expect(result).toMatchInlineSnapshot(`
"const { FlatCompat } = require("@eslint/eslintrc");
const js = require("@eslint/js");
"import { FlatCompat } from "@eslint/eslintrc";
import js from "@eslint/js";
import { fileURLToPath } from "url";
import { dirname } from 'path';
const compat = new FlatCompat({
baseDirectory: __dirname,
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
module.exports = [
export default [
{ rules: { } },
{ ignores: ["src/ignore/to/keep.ts"] },
{ ignores: ["something/else"] }
@ -955,14 +1012,19 @@ module.exports = [
});
it('should remove entire plugin when array with single element', () => {
const content = `const { FlatCompat } = require("@eslint/eslintrc");
const nxEslintPlugin = require("@nx/eslint-plugin");
const js = require("@eslint/js");
const content = `import { FlatCompat } from "@eslint/eslintrc";
import nxEslintPlugin from "@nx/eslint-plugin";
import js from "@eslint/js";
import { fileURLToPath } from "url";
import { dirname } from 'path';
const compat = new FlatCompat({
baseDirectory: __dirname,
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
module.exports = [
export default [
{ plugins: ["@nx"] },
{ ignores: ["src/ignore/to/keep.ts"] },
{ ignores: ["something/else"] }
@ -970,13 +1032,18 @@ module.exports = [
const result = removePlugin(content, '@nx', '@nx/eslint-plugin');
expect(result).toMatchInlineSnapshot(`
"const { FlatCompat } = require("@eslint/eslintrc");
const js = require("@eslint/js");
"import { FlatCompat } from "@eslint/eslintrc";
import js from "@eslint/js";
import { fileURLToPath } from "url";
import { dirname } from 'path';
const compat = new FlatCompat({
baseDirectory: __dirname,
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
module.exports = [
export default [
{ ignores: ["src/ignore/to/keep.ts"] },
{ ignores: ["something/else"] }
];"
@ -986,14 +1053,18 @@ module.exports = [
describe('removeCompatExtends', () => {
it('should remove compat extends from config', () => {
const content = `const { FlatCompat } = require("@eslint/eslintrc");
const nxEslintPlugin = require("@nx/eslint-plugin");
const js = require("@eslint/js");
const content = `import { FlatCompat } from "@eslint/eslintrc";
import nxEslintPlugin from "@nx/eslint-plugin";
import js from "@eslint/js";
import { fileURLToPath } from "url";
import { dirname } from "path";
const compat = new FlatCompat({
baseDirectory: __dirname,
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
module.exports = [
export default [
{ plugins: { "@nx": nxEslintPlugin } },
...compat.config({ extends: ["plugin:@nx/typescript"] }).map(config => ({
...config,
@ -1013,14 +1084,18 @@ module.exports = [
'plugin:@nx/javascript',
]);
expect(result).toMatchInlineSnapshot(`
"const { FlatCompat } = require("@eslint/eslintrc");
const nxEslintPlugin = require("@nx/eslint-plugin");
const js = require("@eslint/js");
"import { FlatCompat } from "@eslint/eslintrc";
import nxEslintPlugin from "@nx/eslint-plugin";
import js from "@eslint/js";
import { fileURLToPath } from "url";
import { dirname } from "path";
const compat = new FlatCompat({
baseDirectory: __dirname,
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
module.exports = [
export default [
{ plugins: { "@nx": nxEslintPlugin } },
{
files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
@ -1039,9 +1114,10 @@ module.exports = [
describe('removePredefinedConfigs', () => {
it('should remove config objects and import', () => {
const content = stripIndents`
const nx = require("@nx/eslint-plugin");
const playwright = require('eslint-plugin-playwright');
module.exports = [
import nx from "@nx/eslint-plugin";
import playwright from 'eslint-plugin-playwright';
export default [
...nx.config['flat/base'],
...nx.config['flat/typescript'],
...nx.config['flat/javascript'],
@ -1058,8 +1134,9 @@ module.exports = [
expect(result).toMatchInlineSnapshot(`
"
const playwright = require('eslint-plugin-playwright');
module.exports = [
import playwright from 'eslint-plugin-playwright';
export default [
playwright.configs['flat/recommended'],
];"
`);
@ -1067,9 +1144,10 @@ module.exports = [
it('should keep configs that are not in the list', () => {
const content = stripIndents`
const nx = require("@nx/eslint-plugin");
const playwright = require('eslint-plugin-playwright');
module.exports = [
import nx from "@nx/eslint-plugin";
import playwright from 'eslint-plugin-playwright';
export default [
...nx.config['flat/base'],
...nx.config['flat/typescript'],
...nx.config['flat/javascript'],
@ -1086,9 +1164,10 @@ module.exports = [
);
expect(result).toMatchInlineSnapshot(`
"const nx = require("@nx/eslint-plugin");
const playwright = require('eslint-plugin-playwright');
module.exports = [
"import nx from "@nx/eslint-plugin";
import playwright from 'eslint-plugin-playwright';
export default [
...nx.config['flat/react'],
playwright.configs['flat/recommended'],
];"

View File

@ -26,7 +26,10 @@ export function removeOverridesFromLintConfig(content: string): string {
ts.ScriptKind.JS
);
const exportsArray = findAllBlocks(source);
const format = content.includes('export default') ? 'mjs' : 'cjs';
const exportsArray =
format === 'mjs' ? findExportDefault(source) : findModuleExports(source);
if (!exportsArray) {
return content;
}
@ -47,7 +50,19 @@ export function removeOverridesFromLintConfig(content: string): string {
return applyChangesToString(content, changes);
}
function findAllBlocks(source: ts.SourceFile): ts.NodeArray<ts.Node> {
// TODO Change name
function findExportDefault(source: ts.SourceFile): ts.NodeArray<ts.Node> {
return ts.forEachChild(source, function analyze(node) {
if (
ts.isExportAssignment(node) &&
ts.isArrayLiteralExpression(node.expression)
) {
return node.expression.elements;
}
});
}
function findModuleExports(source: ts.SourceFile): ts.NodeArray<ts.Node> {
return ts.forEachChild(source, function analyze(node) {
if (
ts.isExpressionStatement(node) &&
@ -86,7 +101,9 @@ export function hasOverride(
true,
ts.ScriptKind.JS
);
const exportsArray = findAllBlocks(source);
const format = content.includes('export default') ? 'mjs' : 'cjs';
const exportsArray =
format === 'mjs' ? findExportDefault(source) : findModuleExports(source);
if (!exportsArray) {
return false;
}
@ -120,6 +137,7 @@ function parseTextToJson(text: string): any {
.replace(/\s([a-zA-Z0-9_]+)\s*:/g, ' "$1": ')
// stringify any require calls to avoid JSON parsing errors, turn them into just the string value being required
.replace(/require\(['"]([^'"]+)['"]\)/g, '"$1"')
.replace(/\(?await import\(['"]([^'"]+)['"]\)\)?/g, '"$1"')
);
}
@ -141,7 +159,9 @@ export function replaceOverride(
true,
ts.ScriptKind.JS
);
const exportsArray = findAllBlocks(source);
const format = content.includes('export default') ? 'mjs' : 'cjs';
const exportsArray =
format === 'mjs' ? findExportDefault(source) : findModuleExports(source);
if (!exportsArray) {
return content;
}
@ -174,20 +194,24 @@ export function replaceOverride(
let updatedData = update(data);
if (updatedData) {
updatedData = mapFilePaths(updatedData);
const parserReplacement =
format === 'mjs'
? (parser: string) => `(await import('${parser}'))`
: (parser: string) => `require('${parser}')`;
changes.push({
type: ChangeType.Insert,
index: start,
// NOTE: Indentation added to format without formatting tools like Prettier.
text:
' ' +
JSON.stringify(updatedData, null, 2)
// restore any parser require calls that were stripped during JSON parsing
.replace(/"parser": "([^"]+)"/g, (_, parser) => {
return `"parser": require('${parser}')`;
})
.slice(2, -2) // remove curly braces and start/end line breaks since we are injecting just properties
// Append indentation so file is formatted without Prettier
.replaceAll(/\n/g, '\n '),
.replace(
/"parser": "([^"]+)"/g,
(_, parser) => `"parser": ${parserReplacement(parser)}`
)
.slice(2, -2) // Remove curly braces and start/end line breaks
.replaceAll(/\n/g, '\n '), // Maintain indentation
});
}
}
@ -198,7 +222,12 @@ export function replaceOverride(
}
/**
* Adding require statement to the top of the file
* Adding import statement to the top of the file
* The imports are added based on a few rules:
* 1. If it's a default import and matches the variable, return content unchanged.
* 2. If it's a named import and the variables are not part of the import object, add them.
* 3. If no existing import and variable is a string, add a default import.
* 4. If no existing import and variable is an array, add it as an object import.
*/
export function addImportToFlatConfig(
content: string,
@ -214,6 +243,159 @@ export function addImportToFlatConfig(
ts.ScriptKind.JS
);
const format = content.includes('export default') ? 'mjs' : 'cjs';
if (format === 'mjs') {
return addESMImportToFlatConfig(source, printer, content, variable, imp);
}
return addCJSImportToFlatConfig(source, printer, content, variable, imp);
}
function addESMImportToFlatConfig(
source: ts.SourceFile,
printer: ts.Printer,
content: string,
variable: string | string[],
imp: string
): string {
let existingImport: ts.ImportDeclaration | undefined;
ts.forEachChild(source, (node) => {
if (
ts.isImportDeclaration(node) &&
ts.isStringLiteral(node.moduleSpecifier) &&
node.moduleSpecifier.text === imp
) {
existingImport = node;
}
});
// Rule 1:
if (
existingImport &&
typeof variable === 'string' &&
existingImport.importClause?.name?.getText() === variable
) {
return content;
}
// Rule 2:
if (
existingImport &&
existingImport.importClause?.namedBindings &&
Array.isArray(variable)
) {
const namedImports = existingImport.importClause
.namedBindings as ts.NamedImports;
const existingElements = namedImports.elements;
// Filter out variables that are already imported
const newVariables = variable.filter(
(v) => !existingElements.some((e) => e.name.getText() === v)
);
if (newVariables.length === 0) {
return content;
}
const newImportSpecifiers = newVariables.map((v) =>
ts.factory.createImportSpecifier(
false,
undefined,
ts.factory.createIdentifier(v)
)
);
const lastElement = existingElements[existingElements.length - 1];
const insertIndex = lastElement
? lastElement.getEnd()
: namedImports.getEnd();
const insertText = printer.printList(
ts.ListFormat.NamedImportsOrExportsElements,
ts.factory.createNodeArray(newImportSpecifiers),
source
);
return applyChangesToString(content, [
{
type: ChangeType.Insert,
index: insertIndex,
text: `, ${insertText}`,
},
]);
}
// Rule 3:
if (!existingImport && typeof variable === 'string') {
const defaultImport = ts.factory.createImportDeclaration(
undefined,
ts.factory.createImportClause(
false,
ts.factory.createIdentifier(variable),
undefined
),
ts.factory.createStringLiteral(imp)
);
const insert = printer.printNode(
ts.EmitHint.Unspecified,
defaultImport,
source
);
return applyChangesToString(content, [
{
type: ChangeType.Insert,
index: 0,
text: `${insert}\n`,
},
]);
}
// Rule 4:
if (!existingImport && Array.isArray(variable)) {
const objectImport = ts.factory.createImportDeclaration(
undefined,
ts.factory.createImportClause(
false,
undefined,
ts.factory.createNamedImports(
variable.map((v) =>
ts.factory.createImportSpecifier(
false,
undefined,
ts.factory.createIdentifier(v)
)
)
)
),
ts.factory.createStringLiteral(imp)
);
const insert = printer.printNode(
ts.EmitHint.Unspecified,
objectImport,
source
);
return applyChangesToString(content, [
{
type: ChangeType.Insert,
index: 0,
text: `${insert}\n`,
},
]);
}
}
function addCJSImportToFlatConfig(
source: ts.SourceFile,
printer: ts.Printer,
content: string,
variable: string | string[],
imp: string
): string {
const foundBindingVars: ts.NodeArray<ts.BindingElement> = ts.forEachChild(
source,
function analyze(node) {
@ -322,6 +504,22 @@ export function addImportToFlatConfig(
]);
}
function existsAsNamedOrDefaultImport(
node: ts.ImportDeclaration,
variable: string | string[]
) {
const isNamed =
node.importClause.namedBindings &&
ts.isNamedImports(node.importClause.namedBindings);
if (Array.isArray(variable)) {
return isNamed || variable.includes(node.importClause?.name?.getText());
}
return (
(node.importClause.namedBindings &&
ts.isNamedImports(node.importClause.namedBindings)) ||
node.importClause?.name?.getText() === variable
);
}
/**
* Remove an import from flat config
*/
@ -338,8 +536,49 @@ export function removeImportFromFlatConfig(
ts.ScriptKind.JS
);
const format = content.includes('export default') ? 'mjs' : 'cjs';
if (format === 'mjs') {
return removeImportFromFlatConfigESM(source, content, variable, imp);
} else {
return removeImportFromFlatConfigCJS(source, content, variable, imp);
}
}
function removeImportFromFlatConfigESM(
source: ts.SourceFile,
content: string,
variable: string,
imp: string
): string {
const changes: StringChange[] = [];
ts.forEachChild(source, (node) => {
// we can only combine object binding patterns
if (
ts.isImportDeclaration(node) &&
ts.isStringLiteral(node.moduleSpecifier) &&
node.moduleSpecifier.text === imp &&
node.importClause &&
existsAsNamedOrDefaultImport(node, variable)
) {
changes.push({
type: ChangeType.Delete,
start: node.pos,
length: node.end - node.pos,
});
}
});
return applyChangesToString(content, changes);
}
function removeImportFromFlatConfigCJS(
source: ts.SourceFile,
content: string,
variable: string,
imp: string
): string {
const changes: StringChange[] = [];
ts.forEachChild(source, (node) => {
// we can only combine object binding patterns
if (
@ -367,7 +606,7 @@ export function removeImportFromFlatConfig(
}
/**
* Injects new ts.expression to the end of the module.exports array.
* Injects new ts.expression to the end of the module.exports or export default array.
*/
export function addBlockToFlatConfigExport(
content: string,
@ -385,6 +624,79 @@ export function addBlockToFlatConfigExport(
ts.ScriptKind.JS
);
const format = content.includes('export default') ? 'mjs' : 'cjs';
// find the export default array statement
if (format === 'mjs') {
return addBlockToFlatConfigExportESM(
content,
config,
source,
printer,
options
);
} else {
return addBlockToFlatConfigExportCJS(
content,
config,
source,
printer,
options
);
}
}
function addBlockToFlatConfigExportESM(
content: string,
config: ts.Expression | ts.SpreadElement,
source: ts.SourceFile,
printer: ts.Printer,
options: { insertAtTheEnd?: boolean; checkBaseConfig?: boolean } = {
insertAtTheEnd: true,
}
): string {
const exportDefaultStatement = source.statements.find(
(statement): statement is ts.ExportAssignment =>
ts.isExportAssignment(statement) &&
ts.isArrayLiteralExpression(statement.expression)
);
if (!exportDefaultStatement) return content;
const exportArrayLiteral =
exportDefaultStatement.expression as ts.ArrayLiteralExpression;
const updatedArrayElements = options.insertAtTheEnd
? [...exportArrayLiteral.elements, config]
: [config, ...exportArrayLiteral.elements];
const updatedExportDefault = ts.factory.createExportAssignment(
undefined,
false,
ts.factory.createArrayLiteralExpression(updatedArrayElements, true)
);
// update the existing export default array
const updatedStatements = source.statements.map((statement) =>
statement === exportDefaultStatement ? updatedExportDefault : statement
);
const updatedSource = ts.factory.updateSourceFile(source, updatedStatements);
return printer
.printFile(updatedSource)
.replace(/export default/, '\nexport default');
}
function addBlockToFlatConfigExportCJS(
content: string,
config: ts.Expression | ts.SpreadElement,
source: ts.SourceFile,
printer: ts.Printer,
options: { insertAtTheEnd?: boolean; checkBaseConfig?: boolean } = {
insertAtTheEnd: true,
}
): string {
const exportsArray = ts.forEachChild(source, function analyze(node) {
if (
ts.isExpressionStatement(node) &&
@ -443,34 +755,57 @@ export function removePlugin(
true,
ts.ScriptKind.JS
);
const format = content.includes('export default') ? 'mjs' : 'cjs';
const changes: StringChange[] = [];
if (format === 'mjs') {
ts.forEachChild(source, function analyze(node) {
if (
ts.isImportDeclaration(node) &&
ts.isStringLiteral(node.moduleSpecifier) &&
node.moduleSpecifier.text === pluginImport
) {
const importClause = node.importClause;
if (
(importClause && importClause.name) ||
(importClause.namedBindings &&
ts.isNamedImports(importClause.namedBindings))
) {
changes.push({
type: ChangeType.Delete,
start: node.pos,
length: node.end - node.pos,
});
}
}
});
} else {
ts.forEachChild(source, function analyze(node) {
if (
ts.isVariableStatement(node) &&
ts.isVariableDeclaration(node.declarationList.declarations[0]) &&
ts.isCallExpression(node.declarationList.declarations[0].initializer) &&
node.declarationList.declarations[0].initializer.arguments.length &&
ts.isStringLiteral(
node.declarationList.declarations[0].initializer.arguments[0]
) &&
node.declarationList.declarations[0].initializer.arguments[0].text ===
pluginImport
) {
changes.push({
type: ChangeType.Delete,
start: node.pos,
length: node.end - node.pos,
});
}
});
}
ts.forEachChild(source, function analyze(node) {
if (
ts.isVariableStatement(node) &&
ts.isVariableDeclaration(node.declarationList.declarations[0]) &&
ts.isCallExpression(node.declarationList.declarations[0].initializer) &&
node.declarationList.declarations[0].initializer.arguments.length &&
ts.isStringLiteral(
node.declarationList.declarations[0].initializer.arguments[0]
) &&
node.declarationList.declarations[0].initializer.arguments[0].text ===
pluginImport
ts.isExportAssignment(node) &&
ts.isArrayLiteralExpression(node.expression)
) {
changes.push({
type: ChangeType.Delete,
start: node.pos,
length: node.end - node.pos,
});
}
});
ts.forEachChild(source, function analyze(node) {
if (
ts.isExpressionStatement(node) &&
ts.isBinaryExpression(node.expression) &&
node.expression.left.getText() === 'module.exports' &&
ts.isArrayLiteralExpression(node.expression.right)
) {
const blockElements = node.expression.right.elements;
const blockElements = node.expression.elements;
blockElements.forEach((element) => {
if (ts.isObjectLiteralExpression(element)) {
const pluginsElem = element.properties.find(
@ -583,7 +918,15 @@ export function removeCompatExtends(
ts.ScriptKind.JS
);
const changes: StringChange[] = [];
findAllBlocks(source)?.forEach((node) => {
const format = content.includes('export default') ? 'mjs' : 'cjs';
const exportsArray =
format === 'mjs' ? findExportDefault(source) : findModuleExports(source);
if (!exportsArray) {
return content;
}
exportsArray.forEach((node) => {
if (
ts.isSpreadElement(node) &&
ts.isCallExpression(node.expression) &&
@ -644,9 +987,16 @@ export function removePredefinedConfigs(
true,
ts.ScriptKind.JS
);
const format = content.includes('export default') ? 'mjs' : 'cjs';
const changes: StringChange[] = [];
let removeImport = true;
findAllBlocks(source)?.forEach((node) => {
const exportsArray =
format === 'mjs' ? findExportDefault(source) : findModuleExports(source);
if (!exportsArray) {
return content;
}
exportsArray.forEach((node) => {
if (
ts.isSpreadElement(node) &&
ts.isElementAccessExpression(node.expression) &&
@ -709,14 +1059,23 @@ export function addPluginsToExportsBlock(
* Adds compat if missing to flat config
*/
export function addFlatCompatToFlatConfig(content: string) {
let result = content;
result = addImportToFlatConfig(result, 'js', '@eslint/js');
const result = addImportToFlatConfig(content, 'js', '@eslint/js');
const format = content.includes('export default') ? 'mjs' : 'cjs';
if (result.includes('const compat = new FlatCompat')) {
return result;
}
result = addImportToFlatConfig(result, ['FlatCompat'], '@eslint/eslintrc');
const index = result.indexOf('module.exports');
return applyChangesToString(result, [
if (format === 'mjs') {
return addFlatCompatToFlatConfigESM(result);
} else {
return addFlatCompatToFlatConfigCJS(result);
}
}
function addFlatCompatToFlatConfigCJS(content: string) {
content = addImportToFlatConfig(content, ['FlatCompat'], '@eslint/eslintrc');
const index = content.indexOf('module.exports');
return applyChangesToString(content, [
{
type: ChangeType.Insert,
index: index - 1,
@ -729,6 +1088,32 @@ const compat = new FlatCompat({
},
]);
}
function addFlatCompatToFlatConfigESM(content: string) {
const importsToAdd = [
{ variable: 'js', module: '@eslint/js' },
{ variable: ['fileURLToPath'], module: 'url' },
{ variable: ['dirname'], module: 'path' },
{ variable: ['FlatCompat'], module: '@eslint/eslintrc' },
];
for (const { variable, module } of importsToAdd) {
content = addImportToFlatConfig(content, variable, module);
}
const index = content.indexOf('export default');
return applyChangesToString(content, [
{
type: ChangeType.Insert,
index: index - 1,
text: `
const compat = new FlatCompat({
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});\n
`,
},
]);
}
/**
* Generate node list representing the imports and the exports blocks
@ -736,17 +1121,26 @@ const compat = new FlatCompat({
*/
export function createNodeList(
importsMap: Map<string, string>,
exportElements: ts.Expression[]
exportElements: ts.Expression[],
format: 'mjs' | 'cjs'
): ts.NodeArray<
ts.VariableStatement | ts.Identifier | ts.ExpressionStatement | ts.SourceFile
> {
const importsList = [];
// generateRequire(varName, imp, ts.factory);
Array.from(importsMap.entries()).forEach(([imp, varName]) => {
importsList.push(generateRequire(varName, imp));
if (format === 'mjs') {
importsList.push(generateESMImport(varName, imp));
} else {
importsList.push(generateRequire(varName, imp));
}
});
const exports =
format === 'mjs'
? generateESMExport(exportElements)
: generateCJSExport(exportElements);
return ts.factory.createNodeArray([
// add plugin imports
...importsList,
@ -757,21 +1151,33 @@ export function createNodeList(
false,
ts.ScriptKind.JS
),
// creates:
// module.exports = [ ... ];
ts.factory.createExpressionStatement(
ts.factory.createBinaryExpression(
ts.factory.createPropertyAccessExpression(
ts.factory.createIdentifier('module'),
ts.factory.createIdentifier('exports')
),
ts.factory.createToken(ts.SyntaxKind.EqualsToken),
ts.factory.createArrayLiteralExpression(exportElements, true)
)
),
exports,
]);
}
function generateESMExport(elements: ts.Expression[]): ts.ExportAssignment {
// creates: export default = [...]
return ts.factory.createExportAssignment(
undefined,
false,
ts.factory.createArrayLiteralExpression(elements, true)
);
}
function generateCJSExport(elements: ts.Expression[]): ts.ExpressionStatement {
// creates: module.exports = [...]
return ts.factory.createExpressionStatement(
ts.factory.createBinaryExpression(
ts.factory.createPropertyAccessExpression(
ts.factory.createIdentifier('module'),
ts.factory.createIdentifier('exports')
),
ts.factory.createToken(ts.SyntaxKind.EqualsToken),
ts.factory.createArrayLiteralExpression(elements, true)
)
);
}
export function generateSpreadElement(name: string): ts.SpreadElement {
return ts.factory.createSpreadElement(ts.factory.createIdentifier(name));
}
@ -831,17 +1237,20 @@ export function stringifyNodeList(
true,
ts.ScriptKind.JS
);
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')
);
const result = printer
.printList(ts.ListFormat.MultiLine, nodes, resultFile)
// add new line before compat initialization
.replace(
/const compat = new FlatCompat/,
'\nconst compat = new FlatCompat'
);
if (result.includes('export default')) {
return result // add new line before export default = ...
.replace(/export default/, '\nexport default');
} else {
return result.replace(/module.exports/, '\nmodule.exports');
}
}
/**
@ -871,6 +1280,50 @@ export function generateRequire(
);
}
// Top level imports
export function generateESMImport(
variableName: string | ts.ObjectBindingPattern,
imp: string
): ts.ImportDeclaration {
let importClause;
if (typeof variableName === 'string') {
// For single variable import e.g import foo from 'module';
importClause = ts.factory.createImportClause(
false,
ts.factory.createIdentifier(variableName),
undefined
);
} else {
// For object binding pattern import e.g import { a, b, c } from 'module';
importClause = ts.factory.createImportClause(
false,
undefined,
ts.factory.createNamedImports(
variableName.elements.map((element) => {
const propertyName = element.propertyName
? ts.isIdentifier(element.propertyName)
? element.propertyName
: ts.factory.createIdentifier(element.propertyName.getText())
: undefined;
return ts.factory.createImportSpecifier(
false,
propertyName,
element.name as ts.Identifier
);
})
)
);
}
return ts.factory.createImportDeclaration(
undefined,
importClause,
ts.factory.createStringLiteral(imp)
);
}
/**
* FROM: https://github.com/eslint/rewrite/blob/e2a7ec809db20e638abbad250d105ddbde88a8d5/packages/migrate-config/src/migrate-config.js#L222
*
@ -904,7 +1357,8 @@ export function overrideNeedsCompat(
export function generateFlatOverride(
_override: Partial<Linter.ConfigOverride<Linter.RulesRecord>> & {
ignores?: Linter.FlatConfig['ignores'];
}
},
format: 'mjs' | 'cjs'
): ts.ObjectLiteralExpression | ts.SpreadElement {
const override = mapFilePaths(_override);
@ -981,21 +1435,10 @@ export function generateFlatOverride(
}
return propertyAssignment;
} else {
// Change parser to require statement.
return ts.factory.createPropertyAssignment(
'parser',
ts.factory.createCallExpression(
ts.factory.createIdentifier('require'),
undefined,
[
ts.factory.createStringLiteral(
override['languageOptions']?.['parserOptions']?.parser ??
override['languageOptions']?.parser ??
override.parser
),
]
)
);
// Change parser to import statement.
return format === 'mjs'
? generateESMParserImport(override)
: generateCJSParserImport(override);
}
},
});
@ -1103,6 +1546,50 @@ export function generateFlatOverride(
);
}
function generateESMParserImport(
override: Partial<Linter.ConfigOverride<Linter.RulesRecord>> & {
ignores?: Linter.FlatConfig['ignores'];
}
): ts.PropertyAssignment {
return ts.factory.createPropertyAssignment(
'parser',
ts.factory.createAwaitExpression(
ts.factory.createCallExpression(
ts.factory.createIdentifier('import'),
undefined,
[
ts.factory.createStringLiteral(
override['languageOptions']?.['parserOptions']?.parser ??
override['languageOptions']?.parser ??
override.parser
),
]
)
)
);
}
function generateCJSParserImport(
override: Partial<Linter.ConfigOverride<Linter.RulesRecord>> & {
ignores?: Linter.FlatConfig['ignores'];
}
): ts.PropertyAssignment {
return ts.factory.createPropertyAssignment(
'parser',
ts.factory.createCallExpression(
ts.factory.createIdentifier('require'),
undefined,
[
ts.factory.createStringLiteral(
override['languageOptions']?.['parserOptions']?.parser ??
override['languageOptions']?.parser ??
override.parser
),
]
)
);
}
export function generateFlatPredefinedConfig(
predefinedConfigName: string,
moduleName = 'nx',

View File

@ -21,7 +21,7 @@ import { globWithWorkspaceContext } from 'nx/src/utils/workspace-context';
import { gte } from 'semver';
import {
baseEsLintConfigFile,
baseEsLintFlatConfigFile,
BASE_ESLINT_CONFIG_FILENAMES,
ESLINT_CONFIG_FILENAMES,
isFlatConfig,
} from '../utils/config-file';
@ -405,7 +405,7 @@ function getProjectUsingESLintConfig(
): CreateNodesResult['projects'][string] | null {
const rootEslintConfig = [
baseEsLintConfigFile,
baseEsLintFlatConfigFile,
...BASE_ESLINT_CONFIG_FILENAMES,
...ESLINT_CONFIG_FILENAMES,
].find((f) => existsSync(join(context.workspaceRoot, f)));

View File

@ -1,6 +1,9 @@
import { existsSync, statSync } from 'fs';
import { basename, dirname, join, resolve } from 'path';
import { eslintFlatConfigFilenames } from './flat-config';
import {
baseEslintConfigFilenames,
eslintFlatConfigFilenames,
} from './flat-config';
export const ESLINT_FLAT_CONFIG_FILENAMES = eslintFlatConfigFilenames;
@ -18,8 +21,10 @@ export const ESLINT_CONFIG_FILENAMES = [
...ESLINT_FLAT_CONFIG_FILENAMES,
];
export const BASE_ESLINT_CONFIG_FILENAMES = baseEslintConfigFilenames;
export const baseEsLintConfigFile = '.eslintrc.base.json';
export const baseEsLintFlatConfigFile = 'eslint.base.config.cjs';
export const baseEsLintFlatConfigFile = 'eslint.base.config.mjs';
// Make sure we can handle previous file extension as well for migrations or custom generators.
export const legacyBaseEsLintFlatConfigFile = 'eslint.base.config.js';

View File

@ -1,10 +1,17 @@
import { Tree } from '@nx/devkit';
import { gte } from 'semver';
// todo: add support for eslint.config.mjs,
export const eslintFlatConfigFilenames = [
'eslint.config.cjs',
'eslint.config.js',
'eslint.config.mjs',
];
export const baseEslintConfigFilenames = [
'eslint.base.js',
'eslint.base.config.cjs',
'eslint.base.config.js',
'eslint.base.config.mjs',
];
export function getRootESLintFlatConfigFilename(tree: Tree): string {

View File

@ -623,7 +623,41 @@ describe('app', () => {
describe('--linter', () => {
describe('default (eslint)', () => {
it('should add flat config as needed', async () => {
it('should add flat config as needed MJS', async () => {
tree.write('eslint.config.mjs', 'export default {};');
const name = uniq();
await applicationGenerator(tree, {
directory: name,
style: 'css',
});
expect(tree.read(`${name}/eslint.config.mjs`, 'utf-8'))
.toMatchInlineSnapshot(`
"import { FlatCompat } from '@eslint/eslintrc';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import js from '@eslint/js';
import nx from '@nx/eslint-plugin';
import baseConfig from '../eslint.config.mjs';
const compat = new FlatCompat({
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
export default [
...compat.extends('next', 'next/core-web-vitals'),
...baseConfig,
...nx.configs['flat/react-typescript'],
{
ignores: ['.next/**/*'],
},
];
"
`);
});
it('should add flat config as needed CJS', async () => {
tree.write('eslint.config.cjs', '');
const name = uniq();

View File

@ -33,7 +33,7 @@ exports[`app generated files content - as-provided - my-app general application
"
`;
exports[`app generated files content - as-provided - my-app general application should configure eslint correctly (flat config) 1`] = `
exports[`app generated files content - as-provided - my-app general application should configure eslint correctly (flat config CJS) 1`] = `
"const { FlatCompat } = require('@eslint/eslintrc');
const js = require('@eslint/js');
const baseConfig = require('../eslint.config.cjs');
@ -66,6 +66,40 @@ module.exports = [
"
`;
exports[`app generated files content - as-provided - my-app general application should configure eslint correctly (flat config ESM) 1`] = `
"import { FlatCompat } from '@eslint/eslintrc';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import js from '@eslint/js';
import baseConfig from '../eslint.config.mjs';
const compat = new FlatCompat({
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
export default [
...baseConfig,
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.vue'],
// Override or add rules here
rules: {},
},
...compat.extends('@nuxt/eslint-config'),
{
files: ['**/*.vue'],
languageOptions: {
parserOptions: {
parser: await import('@typescript-eslint/parser'),
},
},
},
{
ignores: ['.nuxt/**', '.output/**', 'node_modules'],
},
];
"
`;
exports[`app generated files content - as-provided - my-app general application should configure nuxt correctly 1`] = `
"import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
import { defineNuxtConfig } from 'nuxt/config';
@ -393,7 +427,7 @@ exports[`app generated files content - as-provided - myApp general application s
"
`;
exports[`app generated files content - as-provided - myApp general application should configure eslint correctly (flat config) 1`] = `
exports[`app generated files content - as-provided - myApp general application should configure eslint correctly (flat config CJS) 1`] = `
"const { FlatCompat } = require('@eslint/eslintrc');
const js = require('@eslint/js');
const baseConfig = require('../eslint.config.cjs');
@ -426,6 +460,40 @@ module.exports = [
"
`;
exports[`app generated files content - as-provided - myApp general application should configure eslint correctly (flat config ESM) 1`] = `
"import { FlatCompat } from '@eslint/eslintrc';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import js from '@eslint/js';
import baseConfig from '../eslint.config.mjs';
const compat = new FlatCompat({
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
export default [
...baseConfig,
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.vue'],
// Override or add rules here
rules: {},
},
...compat.extends('@nuxt/eslint-config'),
{
files: ['**/*.vue'],
languageOptions: {
parserOptions: {
parser: await import('@typescript-eslint/parser'),
},
},
},
{
ignores: ['.nuxt/**', '.output/**', 'node_modules'],
},
];
"
`;
exports[`app generated files content - as-provided - myApp general application should configure nuxt correctly 1`] = `
"import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
import { defineNuxtConfig } from 'nuxt/config';

View File

@ -65,8 +65,21 @@ describe('app', () => {
).toMatchSnapshot();
});
it('should configure eslint correctly (flat config)', async () => {
tree.write('eslint.config.cjs', '');
it('should configure eslint correctly (flat config ESM)', async () => {
tree.write('eslint.config.mjs', 'export default {};');
await applicationGenerator(tree, {
directory: name,
unitTestRunner: 'vitest',
});
expect(
tree.read(`${name}/eslint.config.mjs`, 'utf-8')
).toMatchSnapshot();
});
it('should configure eslint correctly (flat config CJS)', async () => {
tree.write('eslint.config.cjs', 'module.exports = {};');
await applicationGenerator(tree, {
directory: name,

View File

@ -277,7 +277,7 @@ exports[`library should ignore test files in tsconfig.lib.json 1`] = `
]
`;
exports[`library should support eslint flat config 1`] = `
exports[`library should support eslint flat config CJS 1`] = `
"const vue = require('eslint-plugin-vue');
const baseConfig = require('../eslint.config.cjs');
@ -301,3 +301,28 @@ module.exports = [
];
"
`;
exports[`library should support eslint flat config ESM 1`] = `
"import vue from 'eslint-plugin-vue';
import baseConfig from '../eslint.config.mjs';
export default [
...baseConfig,
...vue.configs['flat/recommended'],
{
files: ['**/*.vue'],
languageOptions: {
parserOptions: {
parser: await import('@typescript-eslint/parser'),
},
},
},
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.vue'],
rules: {
'vue/multi-word-component-names': 'off',
},
},
];
"
`;

View File

@ -149,7 +149,7 @@ describe('library', () => {
expect(eslintJson).toMatchSnapshot();
});
it('should support eslint flat config', async () => {
it('should support eslint flat config CJS', async () => {
tree.write(
'eslint.config.cjs',
`const { FlatCompat } = require('@eslint/eslintrc');
@ -217,6 +217,76 @@ module.exports = [
);
});
it('should support eslint flat config ESM', async () => {
tree.write(
'eslint.config.mjs',
`import { FlatCompat } from '@eslint/eslintrc';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import js from '@eslint/js';
import nx from '@nx/eslint-plugin';
import baseConfig from '../eslint.config.mjs';
const compat = new FlatCompat({
baseDirectory: dirname(fileURLToPath(import.meta.url)),
recommendedConfig: js.configs.recommended,
});
export default [
{ 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: {
...config.rules,
},
})),
...compat.config({ extends: ['plugin:@nx/javascript'] }).map((config) => ({
...config,
files: ['**/*.js', '**/*.jsx'],
rules: {
...config.rules,
},
})),
...compat.config({ env: { jest: true } }).map((config) => ({
...config,
files: ['**/*.spec.ts', '**/*.spec.tsx', '**/*.spec.js', '**/*.spec.jsx'],
rules: {
...config.rules,
},
})),
]`
);
await libraryGenerator(tree, defaultSchema);
const eslintJson = tree.read('my-lib/eslint.config.mjs', 'utf-8');
expect(eslintJson).toMatchSnapshot();
// assert **/*.vue was added to override in base eslint config
const eslintBaseJson = tree.read('eslint.config.mjs', 'utf-8');
expect(eslintBaseJson).toContain(
`files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.vue'],`
);
});
describe('nested', () => {
it('should update tags and implicitDependencies', async () => {
await libraryGenerator(tree, {

View File

@ -16,9 +16,11 @@ export function updateEslintConfig(
!tree.exists('.eslintrc.json') &&
!tree.exists('eslint.config.js') &&
!tree.exists('eslint.config.cjs') &&
!tree.exists('eslint.config.mjs') &&
!tree.exists('.eslintrc.base.json') &&
!tree.exists('eslint.base.config.js') &&
!tree.exists('eslint.base.config.cjs')
!tree.exists('eslint.base.config.cjs') &&
!tree.exists('eslint.base.config.mjs')
) {
return;
}